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 _site 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="_site", html=True), name="_site")

uvicorn.run(app=app, port=3000)
$ tree --noreport .
.
├── app.py
└── _site
    └── 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:

  1. Parse the If-Modified-Since header into a datetime object that supports comparisons with other datetime objects.
  2. For the requested resource (e.g., index.html), determine the datetime for the Last-Modified response header.
  3. If Last-Modified is less than or equal to If-Modified-Since, the server responds with status 304 Not Modified.
  4. 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!

Ready to Learn More Web Development?

Join my Mailing List to receive 1-2 useful Articles per week.


I send two weekly emails on building performant and resilient Web Applications with Python, JavaScript and PostgreSQL. No spam. Unscubscribe at any time.

Continue Reading?

Here are a few more Articles for you ...


HTTP Caching with ETag and If-None-Match Headers

Learn how to use ETag and If-None-Match headers to limit your web application's resource consumption by preventing data retransfers.

By Christoph Schiessl on Python and FastAPI

Why JavaScript’s undefined Isn’t What You Think It Is

In this informative article, you learn that undefined is not a keyword in JavaScript, and it's up to you to ensure it refers to the value its name suggests.

By Christoph Schiessl on JavaScript

Serving Websites with FastAPI's StaticFiles

Learn how to serve a static site using FastAPI. Perfect for locally testing statically generated websites, for instance, with httpie.

By Christoph Schiessl on Python and FastAPI

Christoph Schiessl

Christoph Schiessl

Independent Consultant + Full Stack Developer


If you hire me, you can rely on more than a decade of experience, which I have collected working on web applications for many clients across multiple industries. My involvement usually focuses on hands-on development work using various technologies like Python, JavaScript, PostgreSQL, or whichever technology we determine to be the best tool for the job. Furthermore, you can also depend on me in an advisory capacity to make educated technological choices for your backend and frontend teams. Lastly, I can help you transition to or improve your agile development processes.