Nginx's if_modified_since
directive (in http_core_module
) is often misunderstood when dealing with dynamic content. While it works perfectly for static files by comparing the If-Modified-Since
request header with the file's modification time, dynamic responses require special handling.
# Basic static file configuration (works out-of-the-box)
location /static/ {
if_modified_since before;
# ...
}
The key difference: For dynamic responses, Nginx does not automatically compare the client's If-Modified-Since
with your application's Last-Modified
response header. You need to implement this logic either:
- In your application code (PHP/Python), or
- Using Nginx's
proxy_cache_valid
when reverse-proxying
For PHP (Laravel example):
// In your controller
$lastModified = Carbon::parse($resource->updated_at);
if ($request->header('If-Modified-Since') &&
strtotime($request->header('If-Modified-Since')) >= $lastModified->timestamp) {
return response()->json([], 304);
}
return response()
->json($data)
->header('Last-Modified', $lastModified->toRfc7231String());
When using PHP-FPM:
location ~ \.php$ {
fastcgi_cache my_cache;
fastcgi_cache_valid 200 304 1h;
fastcgi_cache_use_stale updating error timeout invalid_header;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
add_header X-Cache-Status $upstream_cache_status;
# This makes Nginx respect your app's Last-Modified
fastcgi_pass_header Last-Modified;
fastcgi_pass_header Cache-Control;
# Standard FastCGI config...
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
}
Use this curl command to test if headers are properly propagating:
curl -v -H "If-Modified-Since: $(date -u -d '1 hour ago' +'%a, %d %b %Y %H:%M:%S GMT')" \
http://yoursite.com/api/endpoint
Check for these critical headers in the response:
Last-Modified
(must match your app's timestamp format)Cache-Control
(shouldn't haveno-cache
orprivate
for this use case)- Status code (should be 304 when content hasn't changed)
When implementing Last-Modified
headers for dynamic content in Nginx, many developers notice unexpected behavior with the if_modified_since
directive. The key observation is:
# Typical response from dynamic backend (PHP/Python)
HTTP/1.1 200 OK
Last-Modified: Fri, 01 May 2015 19:56:05 GMT
Content-Type: application/json
# Despite If-Modified-Since header from client:
GET /api/data HTTP/1.1
If-Modified-Since: Fri, 01 May 2015 18:00:00 GMT
Nginx's handling differs between static and dynamic content:
- Static files: Nginx compares modification timestamps directly from filesystem
- Dynamic content: The backend must explicitly handle 304 responses
For dynamic content, you'll need additional configuration:
location ~ \.php$ {
# Ensure PHP application receives the If-Modified-Since header
fastcgi_param HTTP_IF_MODIFIED_SINCE $http_if_modified_since;
# Your regular FastCGI params...
}
Here's a full implementation example for PHP applications:
// In your PHP application
$lastModified = // Your logic to determine last modified time
header("Last-Modified: " . gmdate("D, d M Y H:i:s", $lastModified) . " GMT");
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$ifModifiedSince = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
if ($ifModifiedSince >= $lastModified) {
header('HTTP/1.1 304 Not Modified');
exit;
}
}
Use this curl command to verify your implementation:
curl -I -H "If-Modified-Since: Fri, 01 May 2015 18:00:00 GMT" http://yoursite.com/api
Expected responses:
- 200 OK when content is newer
- 304 Not Modified when content hasn't changed
For more complex scenarios:
# Nginx configuration to cache dynamic responses
proxy_cache_valid 200 302 304 5m;
proxy_cache_key "$scheme$request_method$host$request_uri$http_if_modified_since";
Remember that proper ETag implementation often works better with dynamic content.