When the data has not changed, the server does not need to send the full JSON again. With FastAPI, you just attach an ETag to the response and check the If-None-Match header: if it matches, return 304 Not Modified; if it does not, return the fresh data with a new ETag. This fits best for read endpoints whose data changes slowly, such as categories or configuration.
The Problem
Take the familiar endpoint below. Every time a client calls it, the server returns the full JSON, even if not a single byte of the data has changed:
from fastapi import FastAPIapp = FastAPI()ITEMS = {"id": 1, "name": "Áo thun", "price": 199000}@app.get("/items/1")def get_item(): return ITEMS
With one client that is fine, but with thousands of clients polling every minute, bandwidth and CPU spent serializing JSON are wasted for nothing. Mobile clients also burn battery and mobile data just to receive something they already have.
The Solution
An ETag is an identifier string for one version of the data, usually a hash of its content. The server sends the ETag in the response; next time, the client sends that value back through the If-None-Match header. If it matches, the server returns 304 and sends no body:
import hashlibimport jsonfrom fastapi import FastAPI, Header, Responsefrom fastapi.responses import JSONResponseapp = FastAPI()ITEMS = {"id": 1, "name": "Áo thun", "price": 199000}def make_etag(payload: dict) -> str: body = json.dumps(payload, sort_keys=True, ensure_ascii=False) return f'"{hashlib.md5(body.encode("utf-8")).hexdigest()}"'@app.get("/items/1")def get_item(if_none_match: str | None = Header(default=None)): etag = make_etag(ITEMS) if if_none_match == etag: return Response(status_code=304, headers={"ETag": etag}) return JSONResponse(content=ITEMS, headers={"ETag": etag})
The clever part lies in three details. First, sort_keys=True makes the same data always produce the same hash, regardless of key order in the dict. Second, the ETag comparison happens before the response is serialized, so a cache hit costs almost no resources. Third, the 304 status still carries the ETag so the client can refresh its cache without downloading again. Note: the If-None-Match header can contain multiple ETags or the * character, so in production handle those cases too.
Apply It to Your Project
Start from one heavily read, rarely changed endpoint — a list of provinces, a price table, or published posts. Attach an ETag, measure traffic before and after a week, then roll it out to other endpoints. When data changes per record, include the record’s updated_at or version in the hash input so the ETag changes exactly when the data does.
Related Posts
- FastAPI: Compress Responses with GZipMiddleware in 5 Minutes — Less Bandwidth, No Endpoint Changes
- FastAPI: Return the Right Status Codes and Headers — 201 Created, 204 No Content, and Location in 5 Minutes
- FastAPI: Set a Secure Login Cookie with Response.set_cookie in 5 Minutes
FAQ
What is the difference between a strong and a weak ETag?
A strong ETag, for example “abc123”, asserts that the body is byte-for-byte identical. A weak ETag, starting with W/, only asserts that the content is semantically equivalent. For an ordinary JSON API, a strong ETag computed from a content hash is enough and easiest to verify.
Can ETag be used together with GZipMiddleware?
Yes. The two mechanisms live at different layers: GZip shrinks the body when a 200 must be returned, while ETag drops the body entirely on a 304. When combining them, check the actual headers, because some proxies may change the ETag after compression.
When should you not use ETag?
Do not use it for data that changes continuously in real time, such as stock prices or a moving vehicle’s location, because the client will almost never hit the cache. Also do not use ETag for POST, PUT, or DELETE, since those methods change state rather than read data.
Does the client send If-None-Match automatically?
Browsers do this themselves for cacheable GET responses: they store the ETag and send it back on the next call. With a mobile app or a hand-written script, you need to store the ETag you received and attach it to the If-None-Match header in the next request.
💬 Have you tried this in your project? If you hit an error or know a cleaner way, leave a comment below — I read them all and will add the best ideas to this post.
References
- MDN Web Docs — ETag: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
- MDN Web Docs — If-None-Match: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match
- FastAPI — Response: https://fastapi.tiangolo.com/advanced/response-change-status-code/
