Response header
< Last-Modified: Tue, 15 Nov 2026 12:45:26 GMTLast-Modified
Learn how the Last-Modified header indicates when a resource was last changed. Enable efficient cache validation with If-Modified-Since conditional requests.
- Direction
- Response
- Category
- Caching
- Spec
- RFC 9110 §8.8.2
On this page
- What is Last-Modified?
- How It Works
- Date Format
- Generating Last-Modified
- File System
- Database Records
- Dynamic Content
- Application Deployment
- Conditional Requests
- If-Modified-Since (GET requests)
- If-Unmodified-Since (PUT/DELETE requests)
- Cache Validation Flow
- Performance Benefits
- Bandwidth Savings
- Faster Page Loads
- Real-World Examples
- Blog Posts
- API Data
- Static Assets
- Last-Modified vs ETag
- Using Both Together
- Getting Last-Modified right
- Common Issues
- Related Headers
TL;DR: Indicates when a resource was last changed using a timestamp. Enables cache validation with If-Modified-Since for efficient bandwidth usage.
What is Last-Modified?
Last-Modified tells browsers when a resource was last changed. It’s like a timestamp on a file that says “this was updated on January 15th at 3:42 PM.” Browsers use this information to check if their cached version is still current without downloading the entire resource again.
This header enables efficient cache validation and reduces unnecessary data transfers.
How It Works
1. Server sends resource with timestamp:
HTTP/1.1 200 OK
Last-Modified: Wed, 15 Jan 2025 15:42:30 GMT
Content-Type: text/html
<html>...</html>
2. Browser caches the response and timestamp.
3. Later, browser asks “changed since then?”:
GET /page.html HTTP/1.1
If-Modified-Since: Wed, 15 Jan 2025 15:42:30 GMT
4. If unchanged, server responds:
HTTP/1.1 304 Not Modified
Last-Modified: Wed, 15 Jan 2025 15:42:30 GMT
5. Browser reuses cached version - saves bandwidth and time!
Date Format
Last-Modified uses HTTP date format (RFC 9110 section 5.6.7):
Last-Modified: Wed, 15 Jan 2025 15:42:30 GMT
Format: Day, DD Mon YYYY HH:MM:SS GMT
- Always in GMT/UTC timezone
- Three-letter day and month names
- 24-hour time format
Generating Last-Modified
File System
For static files, use file modification time:
const fs = require('fs')
const stats = fs.statSync('index.html')
const lastModified = stats.mtime.toUTCString()
// "Wed, 15 Jan 2025 15:42:30 GMT"
Database Records
Use record update timestamp:
SELECT *, updated_at FROM articles WHERE id = 1;
const lastModified = new Date(article.updated_at).toUTCString()
Dynamic Content
For generated content, use the most recent data timestamp:
// User dashboard - use latest activity
const lastActivity = Math.max(
user.updated_at,
user.last_login,
...user.notifications.map((n) => n.created_at)
)
const lastModified = new Date(lastActivity).toUTCString()
Application Deployment
For app resources, use build/deploy time:
const buildTime = process.env.BUILD_TIMESTAMP || Date.now()
const lastModified = new Date(buildTime).toUTCString()
Conditional Requests
If-Modified-Since (GET requests)
Check if resource changed since cached version:
GET /api/posts HTTP/1.1
If-Modified-Since: Wed, 15 Jan 2025 15:42:30 GMT
Server responses:
304 Not Modified- No changes, use cached version200 OK- Resource changed, here’s new content
If-Unmodified-Since (PUT/DELETE requests)
Prevent lost updates by ensuring resource hasn’t changed:
PUT /api/posts/123 HTTP/1.1
If-Unmodified-Since: Wed, 15 Jan 2025 15:42:30 GMT
Content-Type: application/json
{"title": "Updated Post"}
Server responses:
200 OK- Resource unchanged, update successful412 Precondition Failed- Resource was modified by someone else
Cache Validation Flow
Initial Request:
GET /style.css HTTP/1.1
HTTP/1.1 200 OK
Last-Modified: Wed, 15 Jan 2025 10:30:00 GMT
Cache-Control: max-age=3600
Content-Type: text/css
body { color: blue; }
After Cache Expires:
GET /style.css HTTP/1.1
If-Modified-Since: Wed, 15 Jan 2025 10:30:00 GMT
HTTP/1.1 304 Not Modified
Last-Modified: Wed, 15 Jan 2025 10:30:00 GMT
Cache-Control: max-age=3600
Browser reuses cached CSS for another hour.
Performance Benefits
Bandwidth Savings
File unchanged scenario:
- Without Last-Modified: 50KB transfer
- With Last-Modified: ~200 bytes (304 response)
- Savings: 99.6%
Faster Page Loads
- No content download for unchanged resources
- Instant cache revalidation
- Reduced server load
Real-World Examples
Blog Posts
GET /posts/my-article HTTP/1.1
HTTP/1.1 200 OK
Last-Modified: Mon, 10 Jan 2025 14:20:00 GMT
Content-Type: text/html
<article>...</article>
API Data
GET /api/users/profile HTTP/1.1
HTTP/1.1 200 OK
Last-Modified: Tue, 11 Jan 2025 09:15:30 GMT
Content-Type: application/json
{"name": "John", "email": "john@example.com"}
Static Assets
GET /images/logo.png HTTP/1.1
HTTP/1.1 200 OK
Last-Modified: Fri, 01 Jan 2025 12:00:00 GMT
Content-Type: image/png
[PNG image data]
Last-Modified vs ETag
| Feature | Last-Modified | ETag |
|---|---|---|
| Precision | 1-second resolution | Exact change detection |
| Generation | Use timestamp | Hash content or version |
| Reliability | Can have clock issues | Always accurate |
| Performance | Faster to generate | More CPU to generate |
| Use Case | File-based content | Dynamic/API content |
Using Both Together
HTTP/1.1 200 OK
Last-Modified: Wed, 15 Jan 2025 15:42:30 GMT
ETag: "abc123"
Content-Type: application/json
{"data": "..."}
Browser will use ETag if available, fall back to Last-Modified.
Getting Last-Modified right
1. Always include for cacheable content:
Last-Modified: Wed, 15 Jan 2025 15:42:30 GMT
Cache-Control: max-age=3600
2. Use precise timestamps:
// ✅ Use actual modification time
const lastModified = file.mtime.toUTCString()
// ❌ Don't use current time
const lastModified = new Date().toUTCString()
3. Handle conditional requests:
app.get('/api/data', (req, res) => {
const lastModified = data.updated_at.toUTCString()
const ifModifiedSince = req.headers['if-modified-since']
if (ifModifiedSince === lastModified) {
return res.status(304).end()
}
res.setHeader('Last-Modified', lastModified)
res.json(data)
})
4. Use with optimistic locking:
app.put('/api/posts/:id', (req, res) => {
const post = getPost(req.params.id)
const lastModified = post.updated_at.toUTCString()
const ifUnmodifiedSince = req.headers['if-unmodified-since']
if (ifUnmodifiedSince !== lastModified) {
return res.status(412).json({ error: 'Post was modified' })
}
// Safe to update
updatePost(req.params.id, req.body)
})
Common Issues
Problem: Clock synchronization across servers Solution: Use database timestamps or centralized time source
Problem: 1-second precision not enough Solution: Use ETag for sub-second changes
Problem: Timezone confusion Solution: Always use GMT/UTC in HTTP headers
Related Headers
- If-Modified-Since - Conditional request header
- If-Unmodified-Since - Conditional request for updates
- ETag - More precise cache validation
- Cache-Control - Caching directives
Frequently asked questions
What is the Last-Modified header?
Last-Modified indicates when the resource was last changed. Clients use it with If-Modified-Since for cache validation to avoid re-downloading unchanged content.
How does Last-Modified work with caching?
The server sends Last-Modified with the response. On subsequent requests, the client sends If-Modified-Since. If unchanged, the server returns 304 Not Modified.
What is the difference between Last-Modified and ETag?
Last-Modified has second-level precision and uses timestamps. ETag is more precise and works for any content. Use both for maximum compatibility.
What format does Last-Modified use?
Last-Modified uses HTTP-date format in GMT: "Last-Modified: Wed, 21 Oct 2026 07:28:00 GMT". The date should reflect actual content modification time.
Sources
Related
ETag vs Last-Modified: HTTP Cache Validators
ETag vs Last-Modified: strong and weak validation, one-second timestamps, conditional request precedence, multi-server consistency, and CDN debugging.
ETag Header: Strong vs Weak, If-None-Match and 304 Debugging
ETag identifies a resource version for cache validation and optimistic locking. Strong vs weak ETags, If-None-Match, If-Match, and why nginx gzip weakens them.
Age Header
Learn how the Age header indicates how long a response has been cached in seconds. Understand cache freshness calculations and CDN behavior.
Expires Header
Learn how the Expires header specifies when cached responses become stale. Understand date formats and when to use Expires vs Cache-Control for caching.
Vary Header: Cache Keys, Vary: Origin and Accept-Encoding
Vary lists the request headers a response depends on, so caches keep separate copies. Vary: Accept-Encoding, Vary: Origin for CORS, Vary: *, and CDN cache keys.