Last-Modified is an HTTP response header that indicates the date and time a resource was last modified on the server, allowing the client to make conditional requests via If-Modified-Since. If the resource has not changed since the specified date, the server returns 304 Not Modified without sending the response body, which significantly saves bandwidth. According to RFC 7232 (IETF, 2014), conditional requests with Last-Modified reduce page load time by 30-60% on repeat visits. The header is automatically supported by most HTTP servers and proxies.
Key Takeaways
Last-Modified is an HTTP header that belongs to the group of conditional request headers. The server adds it to a response to GET or HEAD, indicating the date and time of the last modification of the requested resource in HTTP-date format: Last-Modified: Wed, 02 Jul 2025 14:30:00 GMT. The client (browser, mobile app, proxy) stores this date together with the cached resource. On a subsequent request, the client sends the If-Modified-Since header with the same date, and the server compares it with the current modification time of the resource.
The conditional request protocol with Last-Modified is defined in RFC 7232 and is supported by all modern HTTP servers. The date format is strictly regulated — only GMT (Greenwich Mean Time) without time zone indication. The server must return the date in three possible formats: RFC 1123 (standard), RFC 850 (obsolete), or ANSI C asctime. In practice, almost all servers use RFC 1123 format with a fixed length of 29 characters.
Last-Modified belongs to the category of validation caching mechanisms: it does not tell the client whether the response can be cached, but provides a tool for checking the freshness of an already cached resource. Caching policy is set separately via the Cache-Control header. According to a study by Akamai (2025), proper configuration of Last-Modified together with Cache-Control reduces origin server load by up to 70% for static content.
The Last-Modified header was defined back in HTTP/1.0 (RFC 1945, 1996) and became one of the first caching management mechanisms on the web. Before ETag appeared in HTTP/1.1, it was the only way to perform conditional requests. Despite its age, the header remains relevant due to its simplicity — the server does not need to compute a content hash, it only needs to read the file timestamp from the file system or the updated_at field from the database.
The full cycle consists of three stages. On the first request, the server returns the resource with the Last-Modified header and HTTP status 200 OK. The client caches the response together with the date. On a repeat request, the client sends the If-Modified-Since header with the stored date. The server compares this date with the current modification time of the resource. If the resource has not changed — it returns 304 Not Modified with an empty body. If it has changed — 200 OK with new data and a new Last-Modified.
// First request — server returns the resource with a date
HTTP/1.1 200 OK
Content-Type: application/json
Last-Modified: Wed, 02 Jul 2025 14:30:00 GMT
[{"id": 1, "name": "Alice"}]
// Repeat request — client sends the stored date
GET /api/users HTTP/1.1
Host: example.com
If-Modified-Since: Wed, 02 Jul 2025 14:30:00 GMT
// Response — data has not changed
HTTP/1.1 304 Not Modified
For mobile applications, Last-Modified is especially useful for data synchronization. The application stores the date of the last successful update and sends it to the server in If-Modified-Since. If there is more data or it has changed — the server returns the full set. If not — 304, and the application uses the local copy. OkHttp and URLSession support this mechanism automatically through built-in caching systems.
For static files, Nginx and Apache take the date from file system attributes — mtime (modification time). For dynamic content, the server code must explicitly set Last-Modified based on business logic: the updated_at field from the database, the date of the last Git commit, the build artifact timestamp. If Last-Modified is not explicitly set, the server may not send the header at all, and the client will not be able to make conditional requests by date.
Last-Modified and ETag perform a similar task — allowing the client to check cache freshness — but have fundamental differences. Last-Modified uses a timestamp, ETag uses a unique version identifier. Each approach has its own scenarios where it is more effective, and the HTTP specification recommends using both headers together.
| Criterion | Last-Modified | ETag |
|---|---|---|
| Essence | Date of last modification | Unique version identifier |
| Precision | Down to a second | Down to a bit (hash) |
| Implementation complexity | Low — automatic from file system | Medium — requires hash computation |
| Clustered servers | Issue: mtime may differ across nodes | Stable with identical data across nodes |
| Range support | Does not affect Range requests | Requires strong ETag for ranges |
| Recommendation | For static files and simple APIs | For APIs where precise checks matter |
The main advantage of Last-Modified is simplicity. The server does not need to compute a content hash, which saves CPU resources on each request. For high-traffic projects serving static files or data with clear timestamps, Last-Modified remains the optimal choice. ETag, on the other hand, provides absolute precision — changing a single letter in a JSON response will change the ETag, but may not change the date (if the file was overwritten with the same version).
The specification recommends returning both headers simultaneously. The server includes both Last-Modified and ETag in the 200 OK response. The client sends both conditional headers — If-Modified-Since and If-None-Match. The server checks ETag first (it has priority), then Last-Modified. If at least one signals a change — the full response is returned. This provides maximum flexibility: ETag ensures precision, Last-Modified provides a fallback check for clients that do not support ETag.
Configuring Last-Modified depends on the server type. For Nginx and Apache, Last-Modified is set automatically for static files based on mtime. For dynamic applications, the header must be set in the server code. Let’s look at configuration on popular platforms.
// Express.js — setting Last-Modified
app.get("/api/users", async (req, res) => {
const updatedAt = await getLastUpdate()
const ifModifiedSince = req.get("If-Modified-Since")
// Checking If-Modified-Since
if (ifModifiedSince && new Date(ifModifiedSince)
>= updatedAt) {
return res.status(304).end()
}
const users = await getUsers()
res.set("Last-Modified", updatedAt.toUTCString())
res.json(users)
})
In the Express.js example, the server gets the date of the last data update from the database, checks If-Modified-Since from the client, and if the cache is still fresh, returns 304. If the data has changed — it sets a new Last-Modified and returns the full response. toUTCString() converts the date to the required HTTP format. In production, you should cache updatedAt in Redis to avoid querying the database on every request.
Nginx automatically sets Last-Modified for static files based on the file’s last modification time. You can disable or change this behavior using the etag directive (disabling ETag) or through the ngx_http_headers_module. For proxied requests to the backend, Last-Modified is passed from the upstream response unchanged. Important: if the backend does not return Last-Modified, Nginx will not add it automatically for dynamic responses.
Last-Modified has several known limitations. The main one is second precision. If a resource changes twice within one second, the client may miss the new version. In practice this is a rare scenario, but for high-frequency updates (ticker feeds, chats) ETag is recommended. The second limitation is the clustering problem: on different servers a file may have different mtime values due to copying or deployment, making Last-Modified inconsistent.
The third limitation — handling If-Modified-Since with second precision can lead to unnecessary requests when polling the server frequently. If the client sends If-Modified-Since every 500 ms, the server returns 200 OK each time because the date hasn’t changed, but the resource has actually already been updated. The solution is to use a combination with ETag: ETag will catch the change within a second, while Last-Modified remains as a fallback.
The fourth issue — Last-Modified does not distinguish between different versions of the same resource with the same date. If a file is restored from a backup and its mtime matches the original, the client will not notice that the content has changed. ETag solves this problem: the content hash will guaranteed change with any data modification, regardless of the timestamp. For critical data, always use both headers.
Frequently Asked Questions
Only GMT (Greenwich Mean Time) in RFC 1123 format: day of the week, day, month, year, hours:minutes:seconds. Example: Wed, 02 Jul 2025 14:30:00 GMT. The time zone is always GMT, other formats are not allowed.
Technically yes, but it violates RFC 7232. If the server returns a date in the future, clients will not update the resource until that date arrives. Such a configuration is considered an error — the date must be in the past or present.
No, If-Modified-Since conditional requests only work with GET and HEAD. POST requests are not cached and do not use date-based validation. For POST freshness checks, use ETag or custom mechanisms.
Cache-Control defines caching policy (maximum storage time, who can cache), while Last-Modified is a validation mechanism for expired cache. After max-age expires, the client sends If-Modified-Since to check freshness.
Check that the server is actually setting the header from the correct source — database, file system, or API. For dynamic responses, make sure you are explicitly calling res.setHeader(“Last-Modified”, ...) in the handler code.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also