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:
| Backend | Where it stores data | Fits |
|---|---|---|
SimpleCache | In-process Python memory, on the single running server process | Local development, or a genuinely single-server app |
RedisCache | A separate Redis server, shared by every process talking to it | Real 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