Caching

Some things Bookshelf computes are genuinely expensive relative to how often they actually change — a "top rated books across all users" page, or a count of books per shelf computed by scanning every book row. Recomputing that same expensive result on every single request, even when the underlying data hasn't changed since the last request, is wasted work. Caching is simply storing the result of an expensive operation once, so future requests can reuse it instead of redoing the work.

Flask-Caching

Flask-Caching is the standard extension for this in Flask. Like every extension covered so far, it follows the same create-at-module-level, initialize-in-the-factory pattern:

pip install flask-caching

Example: Initializing Cache in the App Factory

# extensions.py
from flask_caching import Cache

cache = Cache()
# app.py
def create_app():
    app = Flask(__name__)
    # ...other config...
    cache.init_app(app, config={'CACHE_TYPE': 'SimpleCache'})
    return app

Cache Backends

CACHE_TYPE chooses where cached values actually live. The choice matters more than it first appears:

BackendWhere it stores dataFits
SimpleCacheIn-process Python memory, on the single running server processLocal development, or a genuinely single-server app
RedisCacheA separate Redis server, shared by every process talking to itReal production deployments — especially once Bookshelf runs behind multiple Gunicorn worker processes (see Deployment)

The distinction matters because SimpleCache's in-process memory is not shared: if Bookshelf runs as four separate Gunicorn worker processes in production, each with its own SimpleCache, a value cached inside worker #1 is invisible to a request that lands on worker #2. RedisCache solves that by keeping one shared cache external to every worker process.

Caching an Entire View

The simplest, highest-impact form of caching is caching a whole rendered view for a set number of seconds. This fits a read-heavy, rarely-changing page especially well — a public "top rated books" page that every visitor sees identically is a good candidate:

Example: Caching the Top Rated Books View

# app.py
@app.route('/top-rated')
@cache.cached(timeout=300)
def top_rated_books():
    top_books = (
        Book.query
        .filter(Book.rating >= 4)
        .order_by(Book.rating.desc())
        .limit(20)
        .all()
    )
    return render_template('top_rated.html', books=top_books)

The first request after a cache miss runs the query and renders the template normally, then stores the finished HTML. Every request for the next 300 seconds gets that stored HTML back directly — the query and the template rendering are both skipped entirely.

Caching a Specific Value, Not a Whole View

Sometimes only one specific, expensive computed value needs caching — not an entire view's output. cache.set() and cache.get() give direct, lower-level control for exactly that:

# An expensive aggregate: books per shelf, across every user
def get_books_per_shelf_counts():
    cached = cache.get('books_per_shelf_counts')
    if cached is not None:
        return cached

    counts = (
        db.session.query(Shelf.name, db.func.count(Book.id))
        .join(Book)
        .group_by(Shelf.name)
        .all()
    )
    cache.set('books_per_shelf_counts', counts, timeout=600)
    return counts

This pattern — check the cache first, compute and store only on a miss — is the manual version of what @cache.cached() does automatically for a whole view, useful whenever the thing worth caching is a single value used in several different places rather than one view's complete output.

Memoizing a Function

@cache.memoize() is a Flask-Caching-specific decorator worth knowing about: it caches a function's return value keyed on the specific arguments it was called with, so books_for_shelf(3) and books_for_shelf(7) are cached separately and automatically, without writing the manual key-building shown above.

@cache.memoize(timeout=300)
def books_for_shelf(shelf_id):
    return Book.query.filter_by(shelf_id=shelf_id).all()

The Hard Problem: Cache Invalidation

Beyond the well-worn joke that cache invalidation is one of the genuinely hard problems in computer science, there's a real practical issue worth taking seriously: a cached value can go stale. If a user rates a new book highly right after /top-rated was cached, that book simply won't appear on the cached page until the 300-second timeout expires and the view recomputes — even though the underlying data already changed.

One common, practical mitigation is clearing the specific affected cache key manually, right after the database write that just made it stale:

def rate_book(book_id, rating):
    book = Book.query.get(book_id)
    book.rating = rating
    db.session.commit()
    cache.delete('books_per_shelf_counts')  # force the next read to recompute
Note: There's no single universal fix for staleness — it's a genuine tradeoff between how fresh data needs to be and how much load caching is meant to remove. A shorter timeout means fresher data and less benefit from caching; a longer one means more benefit and more risk of showing something out of date.
Next up: Background Tasks with Celery covers a different performance problem — an operation too slow to run inside the request/response cycle at all, cached or not.