Many web applications append timestamps or random strings to image URLs as cache-busters. While this technique ensures clients get fresh content, it wreaks havoc on server-side caching when the actual file content doesn't change. Here's a typical example we need to handle:
http://localhost/tiles/world/t/-1_0/-27_23.png?1381358434308
The key solution lies in customizing the cache key. By default, Nginx includes the full URL (including query string) in the cache key. We need to modify this behavior specifically for PNG files.
location ~* \.png$ {
proxy_pass http://backend;
proxy_cache my-cache;
proxy_cache_key "$scheme://$host$uri";
proxy_cache_valid 200 302 60m;
}
For a map tile server scenario, we can implement a more sophisticated solution that:
- Ignores query strings for caching
- Sets appropriate cache headers
- Handles both HTTP and HTTPS
Here's the complete configuration:
http {
proxy_cache_path /path/to/cache levels=1:2 keys_zone=tile_cache:100m inactive=7d;
server {
listen 80;
location ~* ^/tiles/.+\.png$ {
proxy_pass http://tile_backend;
proxy_cache tile_cache;
proxy_cache_key "$scheme://$host$uri";
proxy_cache_valid 200 302 7d;
proxy_cache_use_stale error timeout updating;
add_header X-Cache-Status $upstream_cache_status;
# Handle cache control headers
expires 30d;
add_header Cache-Control "public";
}
}
}
Verify your setup with these curl commands:
# First request (miss)
curl -I http://yourserver/tiles/world/1/2/3.png?12345
# Second request (hit)
curl -I http://yourserver/tiles/world/1/2/3.png?67890
Look for these headers in the response:
X-Cache-Status: HIT
Cache-Control: public, max-age=2592000
Expires: ...
For high-traffic tile servers, consider these optimizations:
- Increase the
proxy_cache_path
size - Adjust
inactive
time based on update frequency - Use
proxy_cache_lock
to prevent cache stampede
When dealing with map tiles or other static assets that include cache-busting query strings (like ?1381358434308
), Nginx's default behavior treats each unique URL as a separate resource - even when only the query parameters differ. This defeats caching for frequently accessed files like PNG tiles.
Here's the proper way to configure Nginx to ignore query strings for specific file types:
http {
proxy_cache_path /path/to/cache levels=1:2 keys_zone=tile_cache:10m inactive=60m;
server {
location ~* \.(png|jpg|jpeg|gif)$ {
proxy_pass http://backend;
proxy_cache tile_cache;
proxy_cache_key "$scheme://$host$uri";
proxy_cache_valid 200 302 60m;
proxy_cache_use_stale error timeout invalid_header updating;
add_header X-Cache-Status $upstream_cache_status;
}
}
}
- proxy_cache_key: The magic happens here by excluding
$args
from the cache key - proxy_cache_valid: Sets cache duration for successful responses
- proxy_cache_use_stale: Serves stale content during backend issues
Verify with curl commands:
curl -I http://yourserver/tiles/1.png?timestamp=123
curl -I http://yourserver/tiles/1.png?timestamp=456
Both requests should return the same X-Cache-Status: HIT
after the first request.
For map tile servers, consider adding these optimizations:
location ~* \.(png|jpg)$ {
# ... existing directives ...
proxy_cache_lock on;
proxy_cache_lock_age 10s;
proxy_cache_lock_timeout 3s;
proxy_cache_min_uses 2;
proxy_cache_revalidate on;
}
Here's a complete working configuration for a tile server:
proxy_cache_path /var/cache/nginx/tiles levels=1:2 keys_zone=TILES:100m inactive=1w;
server {
listen 80;
server_name tiles.example.com;
location / {
proxy_pass http://upstream_tile_server;
proxy_cache TILES;
proxy_cache_key "$scheme://$host$uri";
proxy_cache_valid 200 302 7d;
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
proxy_ignore_headers Cache-Control;
expires max;
}
}