python 93 lines · 2 tabs

Flask ETag Caching for an Expensive Endpoint with Conditional 304 Handling

Shared by codesnips Aug 2026
2 tabs
import hashlib
from functools import wraps

from flask import request, make_response
from redis import Redis

redis = Redis.from_url("redis://localhost:6379/0")


def _make_key(vary_on):
    parts = [request.path, request.query_string.decode("utf-8")]
    for header in vary_on:
        parts.append(f"{header}={request.headers.get(header, '')}")
    return "etag:" + "|".join(parts)


def _compute_etag(body):
    digest = hashlib.sha256(body).hexdigest()
    return f'"{digest}"'


def _not_modified(etag):
    resp = make_response("", 304)
    resp.headers["ETag"] = etag
    resp.headers["Cache-Control"] = "private, max-age=0, must-revalidate"
    return resp


def etag_cache(ttl=300, vary_on=None):
    vary_on = vary_on or []

    def decorator(view):
        @wraps(view)
        def wrapper(*args, **kwargs):
            key = _make_key(vary_on)
            client_tag = request.headers.get("If-None-Match")

            cached = redis.get(key)
            if cached is not None:
                stored_tag = cached.decode("utf-8")
                if client_tag == stored_tag:
                    return _not_modified(stored_tag)

            resp = make_response(view(*args, **kwargs))
            etag = _compute_etag(resp.get_data())
            redis.setex(key, ttl, etag)

            if client_tag == etag:
                return _not_modified(etag)

            resp.headers["ETag"] = etag
            resp.headers["Cache-Control"] = f"private, max-age={ttl}, must-revalidate"
            return resp

        return wrapper

    return decorator
2 files · python Explain with highlit

This snippet shows how an expensive Flask endpoint can be made cheap for repeat callers using HTTP conditional requests. The core idea is that a response body rarely changes between requests, so instead of recomputing and re-sending it every time, the server sends a validator — an ETag — that the client echoes back on its next request via If-None-Match. When the tag still matches, the server answers 304 Not Modified with an empty body, saving both the recomputation and the bandwidth.

In etag_cache.py, the etag_cache decorator wraps a view function. It first builds a stable cache key from the request path and query string via _make_key, then looks in Redis for a previously stored ETag under that key. If a stored tag exists and the incoming If-None-Match header matches it, the view is never called: the decorator short-circuits and returns a bare 304 through _not_modified. This is the fast path and the whole point — the expensive work is skipped entirely on a cache hit.

On a miss, the wrapped view runs, its response is normalized with make_response, and the body is hashed with hashlib.sha256 to derive a content-addressed ETag. That approach means the tag changes if and only if the bytes change, which keeps correctness simple. The tag is stored in Redis with a TTL so stale keys expire, and it is attached to the outgoing response along with Cache-Control. A second identity check compares the freshly computed tag against If-None-Match, covering the case where the Redis entry had expired but the content is in fact unchanged.

In app.py, the decorator sits between Flask's @app.route and the view report, which simulates a slow aggregation with _expensive_report. Because the decorator is transparent, the view stays focused on producing data. Note the trade-offs: weak versus strong ETags matter for range requests, hashing the whole body costs CPU for very large payloads, and the Redis lookup adds a dependency but enables validator sharing across processes. The vary_on argument lets callers include headers like Accept in the key so content negotiation does not serve the wrong variant. This pattern fits read-heavy JSON endpoints where recomputation dominates cost and payloads are moderately sized.


Related snips

Share this code

Here's the card — post it anywhere.

Flask ETag Caching for an Expensive Endpoint with Conditional 304 Handling — share card
Link copied