yum upgrades for production use, this is the repository for you.
Active subscription is required.
NGINX browser caching is one of the fastest wins for website performance. When configured correctly, moreover, it eliminates repeat downloads of CSS, JavaScript, images, and fonts—dramatically improving page load times and reducing server bandwidth.
However, aggressive browser caching without proper cache busting can break your site after deployments. Users will see old CSS with new HTML, leading to broken layouts and frustrating bugs.
Specifically, this comprehensive guide shows you how to configure NGINX browser caching the right way. Mastering NGINX browser caching is essential for modern web performance optimization. You’ll learn the mechanics of Cache-Control and Expires headers, master cache-busting patterns, and deploy production-ready NGINX configurations that you can test and verify.
By the end, you’ll have:
- Deep understanding of how NGINX cache headers work
- Multiple cache-busting strategies with pros and cons
- Copy/paste NGINX configurations for every scenario
- Performance optimizations beyond basic caching
- Verification commands using
nginx -tandcurl -I - Optional one-line immutable caching with GetPageSpeed NGINX Extras

What Browser Caching Really Means
When a browser downloads a static file like app.css, it stores both the file content and the caching rules. These rules come from HTTP response headers sent by your NGINX server.
The most important header is Cache-Control. Essentially, it tells the browser exactly how long to reuse the cached file without asking the server again.
Example headers:
Cache-Control: max-age=31536000, immutable
Expires: Thu, 31 Dec 2037 23:55:55 GMT
What these mean:
max-age=31536000— Cache this file for one year (31,536,000 seconds)immutable— This URL will never change; don’t bother revalidatingExpires— Legacy header for older browsers; sets an absolute expiration date
Critical misconception to avoid: NGINX doesn’t “cache files in the browser.” NGINX sends headers that instruct the browser how to cache. Ultimately, the browser is in control.
How Cache Validation Works
When max-age expires, the browser can revalidate using:
- ETag: A hash of the file content
- Last-Modified: Timestamp of when the file last changed
The browser sends these in subsequent requests:
If-None-Match: "5d8c72a5-4c3"
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT
If the file hasn’t changed, NGINX responds with 304 Not Modified (no body, saving bandwidth). If it has changed, NGINX sends 200 OK with the new content.
The Golden Rule: Cache Busting First
Here’s the problem: If you cache /assets/app.css for one year and then deploy new CSS to that same URL, many users will still see the old version for up to a year.
The solution: Consequently, you must change the URL when the content changes. In other words, this is called cache busting.
The golden rule:
Only set long cache durations (months or years) for URLs that change when their content changes.
This means you need a cache-busting strategy before you enable aggressive caching. Effective NGINX browser caching requires proper cache busting to work correctly.
Understanding NGINX Browser Caching: Cache-Control and Expires Headers
Let’s dive deeper into these headers and how NGINX generates them.
Cache-Control Directives
Cache-Control is the modern, flexible standard. It supports multiple directives:
| Directive | Meaning |
|---|---|
max-age=N |
Cache for N seconds |
public |
Can be cached by browsers and CDNs |
private |
Only cache in the user’s browser, not CDNs |
no-cache |
Store the file, but revalidate before reuse |
no-store |
Don’t cache at all (for sensitive data) |
must-revalidate |
After expiration, must revalidate (can’t serve stale) |
immutable |
File will never change; skip revalidation |
stale-while-revalidate=N |
Serve stale content while revalidating in background |
The Expires Header
Expires is the older standard (HTTP/1.0). It sets an absolute date/time:
Expires: Thu, 31 Dec 2037 23:55:55 GMT
Modern browsers prefer Cache-Control: max-age, but Expires provides a fallback for ancient clients.
How NGINX’s expires Directive Works
Notably, the NGINX expires directive (documented in the official NGINX headers module) is convenient because it sets both Expires and Cache-Control: max-age with one line.
Syntax:
expires [modified] time;
expires epoch | max | off;
Examples:
expires 1y; # Sets max-age=31536000 (1 year)
expires 24h; # Sets max-age=86400 (24 hours)
expires modified +24h; # Expiration = file mtime + 24 hours
expires @15h; # Expire at 3 PM daily
expires -1; # Sets Cache-Control: no-cache
expires epoch; # Sets Expires to Jan 1, 1970 (forces revalidation)
expires max; # Sets Expires to Dec 31, 2037 and max-age to 10 years
expires off; # Don't add/modify headers (default)
Important: When you use expires 1y, NGINX automatically generates:
Expires: [current time + 1 year]
Cache-Control: max-age=31536000
You can then use add_header to append additional directives like immutable.
Recommended NGINX Browser Caching Policy
Here’s a battle-tested policy that balances performance and safety:
1. NGINX Browser Caching for Versioned Static Assets (Cache Busted)
URLs: /assets/app.a3f2c9.css, /js/bundle.12345.js
Policy:
expires 1y;
add_header Cache-Control "public, immutable" always;
Why:
– The URL changes when content changes
– Therefore, safe to cache for the maximum recommended duration (1 year)
– immutable prevents unnecessary revalidation
– public allows Second, CDN caching
2. NGINX Browser Caching for Non-Versioned Static Assets
URLs: /images/logo.png, /favicon.ico
Policy:
expires 1h;
add_header Cache-Control "public, must-revalidate" always;
Why:
– The URL doesn’t change, so use a short cache
– Additionally, browsers will revalidate with If-Modified-Since/If-None-Match
– Furthermore, this still gets 304 responses (fast and bandwidth-efficient)
3. NGINX Browser Caching for HTML Documents
URLs: /, /about/, /products/
Policy:
add_header Cache-Control "no-cache" always;
Why:
– Moreover, HTML often references other assets; you want the latest version
– no-cache means “cache it, but always revalidate”
– As a result, you get fast 304 responses for unchanged HTML
– Consequently, this ensures users get new content immediately after deploy
NGINX Browser Caching: Cache-Busting Strategies Compared
Overall, there are three main strategies. Here’s how they compare:
Strategy 1: Content Hash in Filename for NGINX Browser Caching (Best)
Example: app.a3f2c9.css, bundle.7f8e1d.js
How it works: Your build tool (Webpack, Vite, Rollup) generates filenames with a hash of the file content.
Pros:
– First and foremost, automatic cache busting—hash changes when content changes
– Furthermore, no server-side rewriting needed
– In addition, it works perfectly with CDNs
– Indeed, most common in modern frameworks
Cons:
– However, it requires build tooling
– Every deployment needs updated HTML references
NGINX config: Simple pattern matching (no rewriting needed)
Strategy 2: NGINX Browser Caching with Version/Timestamp in URL
Example: Request /assets/app.1705420000.css but file is /assets/app.css
How it works: HTML references versioned URLs, but NGINX rewrites them to the actual unversioned file.
Pros:
– Additionally, file paths stay simple on disk
– Easy to manage during deployment
– Particularly, good for legacy applications without build tools
Cons:
– On the other hand, it requires NGINX rewrite rules
– Slightly more complex configuration
– Must synchronize version number across all asset references
NGINX config: Uses try_files with a fallback location that strips the version
Strategy 3: Query String Versioning for Browser Caching
Example: /assets/app.css?v=1705420000
How it works: Append a version parameter to the URL.
Pros:
– First, it is extremely simple to implement
– No build tools or rewrites needed
– WordPress uses this by default (style.css?ver=6.7.1)
Cons:
– Nevertheless, some CDNs ignore query strings by default
– Less reliable than filename-based versioning
– Can be harder to reason about in caching policies
Not recommended for new projects, but works acceptably if you’re stuck with it.
NGINX Browser Caching Config: Hashed Filenames
In summary, this is the simplest and most robust setup. Your build tool outputs files like app.3f2c9a1c.css, and NGINX serves them with long cache headers.
NGINX Browser Caching Configuration
# Match files with a hash in the filename
# Examples: app.3f2c9a1c.css, bundle.a1b2c3d4e5f6.js, logo.8f7e6d5c.png
#
# IMPORTANT: The regex is quoted because it contains {8,}.
# Without quotes, NGINX treats { as a config token and the regex breaks.
location ~* "\.([0-9a-f]{8,})\.(css|js|mjs|map|json|png|jpg|jpeg|gif|webp|avif|svg|ico|woff2?|ttf|eot|otf)$" {
# Disable access logging to save disk I/O (see section below)
access_log off;
# Cache for 1 year (max recommended by RFC)
expires 1y;
# Add immutable directive (prevents unnecessary revalidation)
add_header Cache-Control "public, immutable" always;
# Optional: Add CORS headers if serving fonts/assets cross-origin
# add_header Access-Control-Allow-Origin "*" always;
}
Why This NGINX Caching Works
- Regex pattern:
\.([0-9a-f]{8,})\.matches a hash of at least 8 hex characters before the file extension - Quoting: NGINX requires quotes around regexes containing
{to avoid syntax errors - File extensions: Covers all common static assets (CSS, JS, images, fonts)
expires 1y: Sets bothExpiresheader andCache-Control: max-age=31536000add_header ... always: Thealwaysensures the header is added even on error responses
Testing NGINX Browser Caching
Subsequently, after reloading NGINX, test with:
curl -I https://example.com/assets/app.3f2c9a1c.css
Ideally, you should see:
HTTP/1.1 200 OK
Cache-Control: public, immutable, max-age=31536000
Expires: Fri, 18 Jan 2026 12:00:00 GMT
NGINX Browser Caching Config: Versioned URLs with URL Rewriting
This strategy is ideal when you can’t modify filenames on disk (legacy apps, simple deployment processes) but can control the HTML output.
How NGINX Browser Caching Works
- On disk: Files are unversioned (
/assets/app.css,/assets/app.js) - In HTML: References are versioned (
<link href="/assets/app.1705420000.css">) - NGINX rewrites:
/assets/app.1705420000.css→/assets/app.css - Browser sees: Versioned URL with long cache headers
NGINX Browser Caching Configuration (Digit-Based Versioning)
# Match versioned CSS/JS: app.1705420000.css, bundle.12345.js
# IMPORTANT: Place this ABOVE any non-versioned .css/.js location blocks
# (NGINX uses first matching location)
location ~* \.(\d+)\.(css|js)$ {
access_log off;
# Long cache for versioned URLs
expires 1y;
add_header Cache-Control "public, immutable" always;
# Try versioned file first, fallback to stripping version
try_files $uri @css_js_strip_version;
}
# Fallback: strip version and serve unversioned file
location @css_js_strip_version {
# Rewrite: /assets/app.1705420000.css -> /assets/app.css
rewrite ^(.+)\.(\d+)\.(css|js)$ $1.$3 break;
access_log off;
expires 1y;
add_header Cache-Control "public, immutable" always;
}
Why try_files Before Rewrite?
Notably, this pattern is efficient because:
- First, if versioned file exists (e.g., you also deploy with versions), NGINX serves it directly
- If versioned file doesn’t exist, NGINX falls back to the named location
- Named location strips version and serves the unversioned file
- Result: Finally, browser caches the versioned URL for a year, but deployment is simple
Alternative NGINX Caching: Hash-Based Versioning
If you use hashes instead of timestamps (app.a3f2c9.css), adjust the regex:
location ~* "\.([0-9a-f]{6,})\.(css|js)$" {
access_log off;
expires 1y;
add_header Cache-Control "public, immutable" always;
try_files $uri @css_js_strip_version;
}
location @css_js_strip_version {
rewrite ^(.+)\.([0-9a-f]{6,})\.(css|js)$ $1.$3 break;
access_log off;
expires 1y;
add_header Cache-Control "public, immutable" always;
}
Unified Location for Both Versioned and Non-Versioned
If you want one location that handles both scenarios (short cache for non-versioned, long cache for versioned):
location ~* \.(css|js)$ {
access_log off;
# Default: short cache for non-versioned URLs
expires 1h;
add_header Cache-Control "public, must-revalidate" always;
# Try direct file, then try stripping version
try_files $uri @css_js_strip_version;
}
location @css_js_strip_version {
# Strip version: app.12345.css -> app.css
rewrite ^(.+)\.(\d+)\.(css|js)$ $1.$3 break;
# Long cache for versioned URLs
access_log off;
expires 1y;
add_header Cache-Control "public, immutable" always;
}
Caveat: This gives short cache to direct requests for app.css, but long cache to versioned requests like app.12345.css (which rewrite to the same file). This works, but can be confusing if you request both URLs.
NGINX Browser Caching for HTML and Non-Versioned Assets
Obviously, not everything can be cache busted. HTML documents and some static assets need different policies.
HTML: Always Revalidate
HTML documents often reference other versioned assets. Therefore, you want users to get the latest HTML immediately so they load the latest CSS/JS references.
Policy: no-cache (store but revalidate)
location / {
# HTML files (index.html, about.html, etc.)
add_header Cache-Control "no-cache" always;
# Optional: Enable ETag for efficient revalidation
etag on;
}
What this does:
- Initially, browser caches the HTML
- On next visit, browser sends
If-None-Match(ETag) orIf-Modified-Since - If unchanged, then NGINX responds
304 Not Modified(very fast) - If changed, then NGINX sends new HTML with
200 OK
Why not no-store? no-store prevents caching entirely, forcing a full download every time. no-cache is faster because it allows 304 responses.
Non-Versioned Static Assets: Short Cache
For images, fonts, or other files that don’t change often but aren’t versioned:
location ~* \.(png|jpg|jpeg|gif|webp|avif|svg|ico|woff2?|ttf|eot)$ {
access_log off;
# Cache for 1 hour, revalidate after
expires 1h;
add_header Cache-Control "public, must-revalidate" always;
}
Alternative: Longer cache for images
If your images rarely change, you can use a longer duration:
location ~* \.(png|jpg|jpeg|gif|webp|avif|svg)$ {
access_log off;
expires 30d;
add_header Cache-Control "public, must-revalidate" always;
}
Just remember: if you change the image at the same URL, users may see the old version for up to 30 days (unless they force-refresh).
Performance Optimizations Beyond NGINX Browser Caching
Browser caching reduces repeat requests, but you also want the first request to be fast. Fortunately, NGINX has several options to optimize static file delivery:
Enable Efficient File Transmission
# In http { ... } context
sendfile on; # Use kernel sendfile() for zero-copy file transmission
tcp_nopush on; # Send HTTP response headers in one packet with file start
tcp_nodelay on; # Disable Nagle's algorithm for low-latency responses
What these do:
sendfile on: This bypasses userspace copying; kernel sends file data directly to the socket (faster, less CPU)tcp_nopush on: Buffers headers and the start of the file into a single TCP packet (reduces overhead)tcp_nodelay on: Sends small packets immediately instead of waiting to batch them (lower latency)
Collectively, these three together significantly improve static file performance.
Cache File Metadata (open_file_cache)
Additionally, NGINX can cache file metadata (file descriptors, sizes, modification times) to avoid repeated syscalls:
# In http { ... } context
open_file_cache max=10000 inactive=30s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
What these mean:
max=10000: Cache metadata for up to 10,000 filesinactive=30s: Remove cached metadata if not accessed for 30 secondsvalid=60s: Revalidate cached metadata every 60 secondsmin_uses=2: Only cache files accessed at least twice (avoid cache pollution)errors=on: Cache “file not found” errors too (reduces 404 overhead)
Impact: On high-traffic sites serving thousands of static files, this can reduce disk I/O and CPU by 10-20%.
Gzip Compression for Text Files
Similarly, NGINX can compress CSS, JavaScript, and other text files on the fly:
# In http { ... } context
gzip on;
gzip_vary on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
gzip_comp_level 6;
Important: Serve pre-compressed .gz files if available:
gzip_static on; # Serve .css.gz if it exists instead of compressing on-the-fly
This requires your build process to generate .gz files during deployment, but it’s more efficient than runtime compression.
Disk I/O Optimization with access_log off
By default, every request NGINX handles gets logged by default:
192.168.1.100 - - [20/Jan/2026:12:34:56 +0000] "GET /assets/app.css HTTP/1.1" 200 4096
The problem: On high-traffic sites, logging every CSS, JS, and image request can cause significant disk I/O overhead. SSDs can handle it, but However, HDDs struggle, and even SSDs have finite write endurance.
The solution: Therefore, disable access logging for static assets:
location ~* \.(css|js|png|jpg|jpeg|gif|webp|svg|ico|woff2?)$ {
access_log off; # Don't log these requests
# ... rest of config ...
}
When to use access_log off:
- High-traffic sites (thousands of requests/second)
- Static assets that don’t need individual request tracking
- You’re using a CDN (traffic is offloaded anyway)
- Disk I/O is a bottleneck
When NOT to disable:
- You need detailed analytics on static asset usage
- Debugging performance issues (temporarily enable to see what’s being requested)
- Security monitoring (detecting suspicious patterns)
Compromise: Keep access logs for HTML, disable for static assets:
# HTML: keep logging
location / {
access_log /var/log/nginx/access.log combined;
# ...
}
# Static assets: no logging
location ~* \.(css|js|png|jpg|jpeg|gif|webp|svg|ico|woff2?)$ {
access_log off;
# ...
}
The Immutable Module: One-Line Perfect Caching
If you’re using GetPageSpeed NGINX Extras, you can enable an optional module that implements perfect caching in one line.
Installation
On RHEL/CentOS/AlmaLinux/RockyLinux 8+:
sudo dnf install nginx-module-immutable
On RHEL/CentOS 7:
sudo yum install nginx-module-immutable
Enable the NGINX Browser Caching Module
Add to the top of /etc/nginx/nginx.conf (outside the http block):
load_module modules/ngx_http_immutable_module.so;
NGINX Browser Caching Configuration
location /static/ {
immutable on;
}
That’s it. Automatically, the module automatically generates:
Cache-Control: public, max-age=31536000, stale-while-revalidate=31536000, immutable
Expires: Thu, 31 Dec 2037 23:55:55 GMT
What You Get
max-age=31536000: 1-year cache duration (RFC recommended maximum)immutable: Consequently, prevents unnecessary revalidation for cache-busted resourcesstale-while-revalidate=31536000: Allows serving stale content while revalidating (improves cache hit rates)public: Allows CDN caching- Perfect
Expiresheader: Set to the RFC-recommended far-future date (2037)
Why It’s Better Than expires max;
NGINX’s built-in expires max; sets:
Cache-Control: max-age=315360000 # 10 years (exceeds RFC recommendation)
Expires: Thu, 31 Dec 2037 23:55:55 GMT
Overall, the immutable module is better because:
- RFC-compliant: First, it uses 1 year instead of 10 years
- Adds
immutable: Requires manualadd_headerwithexpires - Adds
stale-while-revalidate: Thus improves cache behavior - One line: No need to combine
expires+add_header
Ideal Use Cases
- Frameworks with hash-based cache busting (Webpack, Vite, Next.js, etc.)
- Magento 2 (which uses hash-based asset URLs)
- Any application where asset URLs change when content changes
Verifying Your NGINX Browser Caching Configuration
Importantly, after configuring NGINX, always verify your setup.
Step 1: Test Configuration Syntax
sudo nginx -t
You should see:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
However, if you see errors, do not reload NGINX. then fix the errors first.
Step 2: Reload NGINX
sudo systemctl reload nginx
Or:
sudo nginx -s reload
Step 3: Verify Headers with curl
Test a versioned asset:
curl -I https://example.com/assets/app.a3f2c9.css
Expected output:
HTTP/1.1 200 OK
Server: nginx
Content-Type: text/css
Cache-Control: public, immutable, max-age=31536000
Expires: Fri, 18 Jan 2027 12:00:00 GMT
Test HTML:
curl -I https://example.com/
Expected output:
HTTP/1.1 200 OK
Server: nginx
Content-Type: text/html
Cache-Control: no-cache
Test a non-versioned asset:
curl -I https://example.com/favicon.ico
Expected output:
HTTP/1.1 200 OK
Server: nginx
Content-Type: image/x-icon
Cache-Control: public, must-revalidate, max-age=3600
Expires: [1 hour from now]
Step 4: Test in Browser DevTools
- Open DevTools (F12)
- Go to Network tab
- Reload the page
- Click on a CSS/JS file
- Check the “Headers” section
Look for:
- Response Headers:
Cache-Control,Expires - Status Code on reload:
200(first visit) or304(revalidated) or(disk cache)(served from cache)
Advanced NGINX Browser Caching: CDN Integration
When using a CDN (Cloudflare, Fastly, AWS CloudFront), your NGINX configuration still matters because the CDN respects your origin’s caching headers.
How CDN Caching Works
- First request: Initially, CDN requests file from NGINX origin
- NGINX responds: Subsequently, with
Cache-ControlandExpiresheaders - CDN caches: Then, based on your headers (or CDN-specific rules)
- Subsequent requests: Finally, CDN serves from cache (Thus, NGINX not hit)
Best Practices for CDN + NGINX
1. Use public in Cache-Control:
add_header Cache-Control "public, max-age=31536000, immutable" always;
public explicitly allows shared caches (CDNs) to store the content.
2. Set consistent headers:
Therefore, make sure your max-age matches your caching intent. Some CDNs ignore Expires and only respect Cache-Control.
3. Therefore, use versioned URLs:
Indeed, CDN caching makes cache busting even more critical. Otherwise, a CDN might cache the wrong version across millions of users.
4. Configure CDN cache duration separately:
Fortunately, many CDNs let you override origin headers. For example, Cloudflare’s Edge Cache TTL or AWS CloudFront’s Min/Max TTL. Therefore, set these based on your needs, but always use versioned URLs for long durations.
Advanced: HTTP/2 and HTTP/3 Considerations
Modern HTTP protocols affect caching behavior, though the headers remain the same.
HTTP/2 Benefits
- Multiplexing: One connection handles many requests (reduces overhead)
- Header compression: HPACK reduces header size (faster for repeated headers)
- Server push: (Mostly deprecated, but) could preemptively send cached assets
NGINX HTTP/2 setup:
server {
listen 443 ssl http2;
# ... rest of config ...
}
Nevertheless, caching headers work identically, but multiplexing makes cache misses less expensive.
HTTP/3 (QUIC)
HTTP/3 uses QUIC (UDP-based) instead of TCP. Benefits:
- Faster connection establishment: 0-RTT for returning visitors
- Better handling of packet loss: Thus, no head-of-line blocking
NGINX HTTP/3 setup (requires recent NGINX with QUIC support or OpenResty):
server {
listen 443 ssl http2;
listen 443 quic reuseport;
http3 on;
# ... rest of config ...
}
Again, caching headers are the same. In summary, Essentially, HTTP/3 just makes delivery faster.
Troubleshooting NGINX Browser Caching Issues
Problem: Headers Not Appearing
Symptom: curl -I doesn’t show Cache-Control or Expires.
Causes:
- First, NGINX config not reloaded: Therefore, run
sudo nginx -s reload - Second, location block mismatch: Your regex doesn’t match the URL
- Third, headers only on 200 responses: Add
alwaystoadd_header:
add_header Cache-Control "public, immutable" always;
Without always, headers only appear on successful responses (200, 201, 204, etc.), not on redirects or errors.
Problem: Versioned URLs Return 404
Symptom: /assets/app.12345.css returns 404 even though /assets/app.css exists.
Causes:
- Rewrite rule not working: Therefore, check that the named location
@css_js_strip_versionis defined - Second, location order wrong: Thus, versioned location must come before non-versioned
- Regex doesn’t match: Therefore, test with
location ~* \.(\d+)\.css$and verify with a simple URL
Debug: Add to rewrite location:
location @css_js_strip_version {
error_log /var/log/nginx/rewrite.log debug;
rewrite ^(.+)\.(\d+)\.(css|js)$ $1.$3 break;
}
Then check /var/log/nginx/rewrite.log.
Problem: Caching Too Aggressive (Can’t Update Files)
Symptom: Deployed new CSS, but users still see old version.
Causes:
- First, no cache busting: URL didn’t change, so browsers kept the cached version
- CDN caching: Even if you updated origin, CDN still serves old version
- Third, service worker caching: Progressive Web Apps can cache aggressively
Solutions:
- First, implement cache busting (hash in filename or versioned URLs)
- Second, purge CDN cache after deployment
- Third, update service worker to invalidate caches on version change
Problem: NGINX Crashes or 502 Errors After Config Change
Symptom: Site goes down after nginx -s reload.
Causes:
- First, syntax error: Therefore, run
nginx -tfirst - Second, regex error: Unquoted
{or}in location regex - Third, module not loaded: Trying to use
immutablewithout loading the module
Recovery:
# Revert config
sudo cp /etc/nginx/nginx.conf.bak /etc/nginx/nginx.conf
# Or restore from git
cd /etc/nginx && git checkout nginx.conf
# Reload with known-good config
sudo nginx -s reload
Common Mistakes to Avoid
- Caching non-versioned URLs for a year
Problem: When you deploy, users see old CSS/JS for months.
Fix: Instead, only long-cache versioned URLs.
