Caching with Flask-Caching

Caching stores the result of an expensive operation so repeated requests return the saved copy instantly instead of recomputing it every time.

Learn Caching with Flask-Caching in our free Flask course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…

Part of the free Flask course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

In this lesson you'll see why caching matters, build a working time-to-live cache, learn @cache.cached and @cache.memoize , compare backends, and invalidate stale entries.

Some work is slow — a heavy database query, a call to a remote API, a complex calculation. If many users request the same thing, recomputing it every time wastes time and money. Caching remembers the answer so the next identical request returns instantly.

Every cache entry needs a timeout , also called a TTL (time to live). After it expires the entry is dropped and the next request recomputes a fresh value. The runnable example below builds exactly that — a dictionary mapping each key to its value and an expiry timestamp.

Notice the calls["count"] counter: the expensive function runs only on a cache miss . The second request is served from the cache, so the counter stays at 1. That single number is the whole point of caching.

The first request prints 81 (computed) , the second 81 (cached) , and the expensive function ran exactly once. That is the speedup caching buys you.

@cache.cached keys the cache on the request path, which is perfect for a whole view. But a helper function called with different arguments needs a different cached result per argument set . That is what memoization does — it keys the cache on the function name plus its arguments.

The runnable decorator below mirrors Flask-Caching's @cache.memoize . The runs list records every real computation: calling add(2, 3) twice computes once, but add(10, 1) is a new argument set and recomputes.

The hardest part of caching is keeping it fresh . When the underlying data changes, any cached copy is now wrong. The fix is invalidation : delete the affected key on every write so the next read recomputes.

The runnable example caches a user's name. After a rename writes to the "database", it deletes the cache key, so the next read shows the new value with source: db — proof the stale entry was busted.

Complete the cache below. Replace each ___ so the route caches a computed value with a 5-second TTL and reports whether each response was fresh or cached.

You forgot to invalidate. After any write that affects a cached entry, call cache.delete(key) (or cache.delete_memoized(func, *args) ) so the next read recomputes.

You used SimpleCache with multiple workers, so each process has its own dict. Switch CACHE_TYPE to RedisCache so all workers share one store.

Build a cached Fibonacci route that also tracks how often the cache helped.

Lesson complete — your slow routes just got fast!

You built a TTL cache by hand, learned @cache.cached and @cache.memoize , compared the simple, filesystem, and redis backends, and invalidated stale entries on write.

🚀 Up next: Rate Limiting — protect those fast endpoints from abuse with per-route request limits.

Practice quiz

What does caching do?

  • Encrypts responses
  • Stores an expensive result so repeats return instantly
  • Validates form input
  • Compresses images

Answer: Stores an expensive result so repeats return instantly. Caching stores an expensive result so repeated requests skip recomputation.

What does @cache.cached key the cache on?

  • The request path
  • The function's arguments
  • The current user
  • The HTTP method

Answer: The request path. @cache.cached keys on the request path, ideal for whole view functions.

What does @cache.memoize key the cache on?

  • The request path
  • A fixed timeout only
  • The function's arguments
  • The response status code

Answer: The function's arguments. @cache.memoize keys on the function's arguments, caching each arg set separately.

Which backend is shared across multiple workers and best for production?

  • SimpleCache
  • NullCache
  • FileSystemCache
  • RedisCache

Answer: RedisCache. RedisCache is shared across processes/servers, best for production.

How do you select the cache backend?

  • CACHE_TYPE in the config
  • @cache.backend
  • Cache(backend=...) only
  • An environment file required

Answer: CACHE_TYPE in the config. You set CACHE_TYPE in app.config to choose the backend.

What does TTL (timeout) control?

  • The maximum cache size
  • How long an entry lives before expiring
  • The number of workers
  • The Redis port

Answer: How long an entry lives before expiring. TTL is how long a cached entry lives before it expires and is recomputed.

How do you invalidate a specific memoized result?

  • cache.clear() only
  • cache.flush(func)
  • cache.delete_memoized(func, *args)
  • cache.expire(func)

Answer: cache.delete_memoized(func, *args). cache.delete_memoized(func, *args) invalidates one memoized argument set.

Why does SimpleCache misbehave with multiple workers?

  • It needs Redis to start
  • Each process has its own dict, so invalidation isn't shared
  • It only caches GET requests
  • It expires instantly

Answer: Each process has its own dict, so invalidation isn't shared. SimpleCache lives in one process; each worker has a separate copy.

When should you invalidate a cached entry?

  • Never, rely on TTL only
  • On every read
  • At server startup
  • Whenever you write data the entry depends on

Answer: Whenever you write data the entry depends on. Delete the entry on any write that affects it so the next read recomputes.

When does the expensive function run with a working cache?

  • Only on a cache miss
  • On every request
  • Only on a cache hit
  • Never after the first deploy

Answer: Only on a cache miss. The expensive work runs only on a cache miss; hits are served from the cache.