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 two 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

Disabling 304 Not Modified in FastAPI's StaticFiles

Dealing with caching issues in FastAPI's StaticFiles sub-application and a monkey patching workaround to disable caching.

By Christoph Schiessl on Python and FastAPI

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

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 the more than a decade of experience I have collected 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 also a skilled writer who takes pride in breaking down technical concepts into the simplest possible terms.