HTTP Caching with Last-Modified and If-Modified-Since Headers
          by Christoph Schiessl on Python and FastAPI
In one of my previous articles, I explained the basics of ETag-based caching. Today, I want to introduce you to timestamp-based caching as an alternative, which relies on the Last-Modified response header and the If-Modified-Since request header. Therefore, similar to HTTP caching controlled by the ETag and If-None-Match headers, both client and server-side support are required.
Last-Modified response header
Imagine having a dist directory with a single index.html file and a simple Python application that uses FastAPI's StaticFiles feature to serve this directory.
import uvicorn
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.mount("/", StaticFiles(directory="dist", html=True), name="dist")
uvicorn.run(app=app, port=3000)
$ tree --noreport .
.
├── app.py
└── dist
    └── index.html
If you start your FastAPI app with python app.py and request the index.html file, you'll see the Last-Modified header in the HTTP response:
$ http GET http://localhost:3000/index.html
HTTP/1.1 200 OK
content-length: 62
content-type: text/html; charset=utf-8
date: Sun, 21 Apr 2024 16:22:10 GMT
etag: "d5eabc651bf2490653431623615b1546"
last-modified: Sun, 21 Apr 2024 14:15:02 GMT
server: uvicorn
<!doctype html><meta charset=utf-8><title>/index.html</title>
In the case of FastAPI's StaticFiles, the timestamp comes from the modification timestamp of the file on disk. So, if you touch index.html or modify the file in some other way, the modification timestamp and the value of the header change. However, the timestamp could also come from a different source, such as a database. Please note the timestamp's format: It must be formatted strictly according to the time format defined in RFC 5322. If you ever have to work with RFC 5332-formatted dates, consider using the email.utils module, which is the library that FastAPI uses internally.
If-Modified-Since request header
The opposite side of the Last-Modified header is the If-Modified-Since request header. This header is set by the HTTP client (e.g., your browser) and must adhere to the same formatting. If the request header is present and the HTTP server supports timestamp-based caching, then the following process is followed for incoming requests:
- Parse the 
If-Modified-Sinceheader into adatetimeobject that supports comparisons with otherdatetimeobjects. - For the requested resource (e.g., 
index.html), determine thedatetimefor theLast-Modifiedresponse header. - If 
Last-Modifiedis less than or equal toIf-Modified-Since, the server responds with status304 Not Modified. - Otherwise, the server will handle the request in the normal way and respond with status 
200 OK. 
The whole point of all of this is to save bandwidth by not transferring payloads that the client has already cached. The trick is that 304 Not Modified responses never contain a body, but 200 OK responses do.
$ # 200 OK if 'Last-Modified' > 'If-Modified-Since' ...
$ http GET http://localhost:3000/index.html 'If-Modified-Since: "Sun, 21 Apr 2024 14:15:01 GMT"'
HTTP/1.1 200 OK
content-length: 62
content-type: text/html; charset=utf-8
date: Sun, 21 Apr 2024 16:24:23 GMT
etag: "d5eabc651bf2490653431623615b1546"
last-modified: Sun, 21 Apr 2024 14:15:02 GMT
server: uvicorn
<!doctype html><meta charset=utf-8><title>/index.html</title>
$ # 304 Not Modified if 'Last-Modified' == 'If-Modified-Since' ...
$ http GET http://localhost:3000/index.html 'If-Modified-Since: "Sun, 21 Apr 2024 14:15:02 GMT"'
HTTP/1.1 304 Not Modified
date: Sun, 21 Apr 2024 16:24:08 GMT
etag: "d5eabc651bf2490653431623615b1546"
server: uvicorn
$ # 304 Not Modified if 'Last-Modified' < 'If-Modified-Since' ...
$ http GET http://localhost:3000/index.html 'If-Modified-Since: "Sun, 21 Apr 2024 14:15:03 GMT"'
HTTP/1.1 304 Not Modified
date: Sun, 21 Apr 2024 16:24:08 GMT
etag: "d5eabc651bf2490653431623615b1546"
server: uvicorn
This is everything for today. Thank you for reading, and see you next time!