Showcasing Weak and Strong ETag Headers with FastAPI

by Christoph Schiessl on Python and FastAPI

I recently wrote about HTTP caching with strong ETag headers, but sometimes it makes more sense to use weak ETags. So what's the difference?

Strong vs. Weak ETag Headers

The whole point of ETags is to have a proxy to determine the equality of two resources in HTTP traffic. Instead of directly comparing two potentially very big resources, you can compare their much smaller ETags. This can help you avoid transferring the resource and thereby reduce bandwidth requirements. The difference between strong and weak ETags is how they are calculated.

Strong ETags are derived from a resource in a byte-by-byte fashion. In other words, if the strong ETags of two resources are the same, you can assume that the two resources are literally identical down to the last byte.

In contrast, weak ETags are more liberal because they require only semantic equality, and this is best explained with a simple example.

original_json = b"""{\n  "a": 1,\n  "b": 2\n}"""
minified_json = b"""{"a":1,"b":2}"""

While the two JSON strings above are not byte-by-byte identical, they are semantically equivalent, which means that if you parse them, you get precisely the same data structure for both inputs (i.e., a dictionary with two keys, a and b). So, the strong ETag of these two strings would tell you that they are not identical, but their weak ETag could tell you that they are semantically equivalent.

I'm saying could because there are no fixed rules for this. It's up to you to decide which resource characteristics are insignificant for clients and can be ignored for your use case. I only picked JSON and the removal of insignificant whitespace as an example because they are easy to understand.

Test-Drive with FastAPI

A demonstration says more than 1000 words, so here is a small FastAPI application that is based on the JSON example above and implements four endpoints:

  • PATCH /enable-minification — turns on minification by assigning the global variable current_json to minified_json, thereby changing the return value of subsequent requests.
  • PATCH /disable-minification — turns off minification by assigning the global variable current_json to original_json, thereby changing the return value of subsequent requests.
  • GET /with-strong-etag — returns current_json with strong ETag response header (i.e., MD5 sum of current_json).
  • GET /with-weak-etag — returns current_json with weak ETag response header (i.e., MD5 sum of original_json).

Furthermore, both GET endpoints can also respond with the status 304 Not Modified and without content if the incoming request includes an If-None-Match request header, whose value is identical to the computed value for the ETag response header. This last behavior makes the resources cacheable for clients supporting the ETag/If-None-Match headers.

from fastapi import FastAPI, Request, Response, status
from hashlib import md5
import uvicorn

app = FastAPI()

original_json = b"""{\n  "a": 1,\n  "b": 2\n}"""
minified_json = b"""{"a":1,"b":2}"""
current_json = original_json

@app.patch("/disable-minification", status_code=status.HTTP_204_NO_CONTENT)
def disable_minification() -> None:
  global current_json # needed for the assignment of the global variable
  current_json = original_json

@app.patch("/enable-minification", status_code=status.HTTP_204_NO_CONTENT)
def enable_minification() -> None:
    global current_json
    current_json = minified_json

@app.get("/with-strong-etag")
def with_strong_etag(request: Request) -> Response:
    etag = f'"{md5(current_json).hexdigest()}"'
    return _with_etag(etag, request)

@app.get("/with-weak-etag")
def with_weak_etag(request: Request) -> Response:
    etag = f'\\W"{md5(original_json).hexdigest()}"'
    return _with_etag(etag, request)

def _with_etag(etag: str, request: Request) -> Response:
    common = {"media_type": "application/json", "headers": {"ETag": etag}}
    if request.headers.get("If-None-Match") == etag:
        return Response(status_code=status.HTTP_304_NOT_MODIFIED, **common)
    return Response(status_code=status.HTTP_200_OK, content=current_json, **common)

if __name__ == "__main__":
    uvicorn.run(app=app)

Anyway, you can run this application with python app.py and then use curl to trigger the endpoints. As you can see, if you turn on/off minification, the strong ETag changes, but the weak ETag stays the same.

$ curl -X PATCH localhost:8000/disable-minification # original_json
$ curl -i -s -X GET localhost:8000/with-strong-etag | grep etag
etag: "b4e6790011336adc9d1e96d14780e0ce"
$ curl -i -s -X GET localhost:8000/with-weak-etag | grep etag
etag: \W"b4e6790011336adc9d1e96d14780e0ce"

$ curl -X PATCH localhost:8000/enable-minification # minified_json
$ curl -i -s -X GET localhost:8000/with-strong-etag | grep etag
etag: "608de49a4600dbb5b173492759792e4a"
$ curl -i -s -X GET localhost:8000/with-weak-etag | grep etag
etag: \W"b4e6790011336adc9d1e96d14780e0ce"

This is precisely what I explained before. Weak ETags guarantee semantic equality, and minification doesn't affect the meaning of the JSON. Hence, the weak ETag value doesn't change when the minification is turned on or off. This statement is not true for strong ETags because these guarantee byte-by-byte identicality. Obviously, original_json and minified_json are not identical, and therefore, the strong ETag value changes when minification is turned on or off.

Lastly, we can also demonstrate how the caching behavior differs for strong and weak ETags, but this really shouldn't surprise you after reading this article ...

$ curl -X PATCH localhost:8000/disable-minification # original_json
$ curl -i -s -X GET localhost:8000/with-strong-etag | grep etag
etag: "b4e6790011336adc9d1e96d14780e0ce"
$ curl -i -s -X GET localhost:8000/with-strong-etag \
  --header 'If-None-Match: "b4e6790011336adc9d1e96d14780e0ce"' | head --lines 1
HTTP/1.1 304 Not Modified
$ curl -i -s -X GET localhost:8000/with-weak-etag | grep etag
etag: \W"b4e6790011336adc9d1e96d14780e0ce"
$ curl -i -s -X GET localhost:8000/with-weak-etag \
  --header 'If-None-Match: \W"b4e6790011336adc9d1e96d14780e0ce"' | head --lines 1
HTTP/1.1 304 Not Modified

$ curl -X PATCH localhost:8000/enable-minification # minified_json
$ curl -i -s -X GET localhost:8000/with-strong-etag \
  --header 'If-None-Match: "b4e6790011336adc9d1e96d14780e0ce"' | head --lines 1
HTTP/1.1 200 OK
$ curl -i -s -X GET localhost:8000/with-weak-etag \
  --header 'If-None-Match: \W"b4e6790011336adc9d1e96d14780e0ce"' | head --lines 1
HTTP/1.1 304 Not Modified

That's everything for today. Thank you very much for reading, and see you soon!

Web App Reverse Checklist

Ready to Build Your Next Web App?

Get my Web App Reverse Checklist first ...


Software Engineering is often driven by fashion, but swimming with the current is rarely the best choice. In addition to knowing what to do, it's equally important to know what not to do. And this is precisely what my free Web App Reverse Checklist will help you with.

Subscribe below to get your free copy of my Reverse Checklist delivered to your inbox. Afterward, you can expect one weekly email on building resilient Web Applications using Python, JavaScript, and PostgreSQL.

By the way, it goes without saying that I'm not sharing your email address with anyone, and you're free to unsubscribe at any time. No spam. No commitments. No questions asked.

Continue Reading?

Here are a few more Articles for you ...


HTTP Caching with Last-Modified and If-Modified-Since Headers

Learn about timestamp-based caching in HTTP using the Last-Modified and If-Modified-Since headers, with Python's FastAPI as an example.

By Christoph Schiessl on Python and FastAPI

Using the If-Match Header to Avoid Collisions

Prevent lost updates in HTTP APIs using ETag and If-Match headers to block conflicting updates and ensure data integrity.

By Christoph Schiessl on Python and FastAPI

Restarting uvicorn Workers with the SIGHUP Signal

Learn about process management in Python's uvicorn web server and how to use signals to restart workers and to increment/decrement the number of workers.

By Christoph Schiessl on Python

Christoph Schiessl

Hi, I'm Christoph Schiessl.

I help you build robust and fast Web Applications.


I'm available for hire as a freelance web developer, so you can take advantage of my more than a decade of experience working on many projects across several industries. Most of my clients are building web-based SaaS applications in a B2B context and depend on my expertise in various capacities.

More often than not, my involvement includes hands-on development work using technologies like Python, JavaScript, and PostgreSQL. Furthermore, if you already have an established team, I can support you as a technical product manager with a passion for simplifying complex processes. Lastly, I'm an avid writer and educator who takes pride in breaking technical concepts down into the simplest possible terms.