Chuyển đến nội dung chính

Lesson 6: Nginx Caching

A lesson on Caching in Nginx — browser caching with expires and Cache-Control headers, proxy caching, FastCGI caching for PHP, cache keys and zones. Guide to cache purging, bypass strategies, and optimization techniques. Includes real-world examples to improve performance and reduce load.

🔒 DevSecOps — Lesson 6 Lesson 6: Nginx Caching

Nginx from Basics to Advanced

Part 2: Reverse Proxy & Load Balancing

xdev.asia

TAGS

#Nginx #Caching #ProxyCache #FastCGI #BrowserCache #CacheControl #Expires #Performance #Optimization #CDN #CachePurging #CacheBypass #WebPerformance #StaticContent #DynamicContent #Tutorial #Production #BestPractices #DevOps #Infrastructure


1. Browser Caching with Expires and Cache-Control Headers

Browser caching stores files at the client browser, reducing the number of requests to the server and speeding up load times.

1.1. Expires Header

The Expires header specifies a specific point in time when the cache expires.

Basic syntax:

location ~* \.(jpg|jpeg|png|gif|ico)$ {
    expires 30d;  # Cache for 30 days
}

Expires values:

server {
    listen 80;
    server_name example.com;
    root /var/www/html;
# Images - cache for a long time
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp)$ {
    expires 1y;  # 1 year
    add_header Cache-Control "public, immutable";
}

# CSS and JavaScript - medium cache
location ~* \.(css|js)$ {
    expires 1M;  # 1 month
    add_header Cache-Control "public";
}

# HTML - short cache or no-cache
location ~* \.html$ {
    expires 1h;  # 1 hour
    add_header Cache-Control "public, must-revalidate";
}

# API responses - no cache
location /api/ {
    expires -1;  # No cache
    add_header Cache-Control "no-cache, no-store, must-revalidate";
}

}

Time units:

expires 1s;    # 1 second
expires 5m;    # 5 minutes
expires 2h;    # 2 hours
expires 7d;    # 7 days
expires 3M;    # 3 months (30 days)
expires 1y;    # 1 year

expires -1; # No cache (Cache-Control: no-cache) expires epoch; # Expires: Thu, 01 Jan 1970 00:00:01 GMT expires max; # Expires: Thu, 31 Dec 2037 23:55:55 GMT expires off; # Do not set Expires header

Detailed example:

server {
listen 80;
server_name cdn.example.com;
root /var/www/cdn;

# Versioned assets - cache forever
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf)$ {
    # Only apply to files with version in name
    # e.g., style.v1.2.3.css, app.20240101.js
    if ($uri ~ "\.v[0-9]+\.|\.v?\d{8,}\.") {
        expires max;
        add_header Cache-Control "public, immutable";
    }
    
    # Files without version
    expires 1M;
    add_header Cache-Control "public";
}

# Videos - long cache but can revalidate
location ~* \.(mp4|webm|ogg|avi)$ {
    expires 6M;
    add_header Cache-Control "public, max-age=15552000";
}

# Documents - short cache
location ~* \.(pdf|doc|docx|xls|xlsx)$ {
    expires 1d;
    add_header Cache-Control "public, max-age=86400";
}

}

1.2. Cache-Control Header

Cache-Control is the modern alternative to Expires and is more flexible.

Cache-Control directives:

# public: Can be cached by browser and intermediate caches
add_header Cache-Control "public";

private: Browser-only cache, not cached by proxies

add_header Cache-Control "private";

no-cache: Must revalidate before using cache

add_header Cache-Control "no-cache";

no-store: Do not cache anything

add_header Cache-Control "no-store";

max-age: Cache duration (seconds)

add_header Cache-Control "public, max-age=31536000";

s-maxage: Max-age for shared caches (CDN, proxies)

add_header Cache-Control "public, max-age=3600, s-maxage=86400";

must-revalidate: Must revalidate when stale

add_header Cache-Control "public, max-age=3600, must-revalidate";

immutable: Content never changes

add_header Cache-Control "public, max-age=31536000, immutable";

no-transform: Do not allow content transformation

add_header Cache-Control "public, no-transform";

Combining directives:

server {
listen 80;
server_name example.com;

# Static assets with versioning - immutable
location ~* /static/.*\.(css|js)$ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
}

# Images - public, long cache
location ~* \.(jpg|png|gif|svg)$ {
    expires 6M;
    add_header Cache-Control "public, max-age=15552000";
}

# HTML - short cache with revalidation
location ~* \.html$ {
    expires 10m;
    add_header Cache-Control "public, max-age=600, must-revalidate";
}

# User-specific content - private
location /dashboard/ {
    add_header Cache-Control "private, max-age=300";
}

# Sensitive data - no cache
location /account/ {
    add_header Cache-Control "private, no-cache, no-store, must-revalidate";
    add_header Pragma "no-cache";
    add_header Expires "0";
}

}

1.3. Conditional Caching

map $sent_http_content_type $expires {
default                    off;
text/html                  1h;
text/css                   1M;
application/javascript     1M;
~image/                    1y;
application/pdf            7d;
~font/                     1y;
}

server { listen 80; server_name example.com;

expires $expires;

# Override for specific paths
location /news/ {
    expires 5m;
    add_header Cache-Control "public, max-age=300";
}

}

Cache based on file extension:

server {
listen 80;
root /var/www/html;

# Map file extensions to cache duration
location / {
    if ($request_uri ~* "\.(jpg|jpeg|png|gif|ico)$") {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
    
    if ($request_uri ~* "\.(css|js)$") {
        expires 1M;
        add_header Cache-Control "public";
    }
    
    if ($request_uri ~* "\.html$") {
        expires 1h;
        add_header Cache-Control "public, must-revalidate";
    }
    
    try_files $uri $uri/ =404;
}

}

1.4. ETag and Last-Modified

server {
listen 80;
server_name example.com;

# Enable ETag
etag on;

# Enable Last-Modified header
if_modified_since before;

location / {
    root /var/www/html;
    
    # Browser will send If-None-Match (ETag) or If-Modified-Since
    # Nginx returns 304 Not Modified if file hasn't changed
}

}

Example with conditional requests:

server {
listen 80;
server_name api.example.com;

location /api/data {
    proxy_pass http://backend;
    
    # Pass conditional headers to backend
    proxy_set_header If-Modified-Since $http_if_modified_since;
    proxy_set_header If-None-Match $http_if_none_match;
    
    # Backend returns 304 if not modified
    # Nginx forwards 304 to client
}

}


2. Proxy Caching Basics

Proxy caching stores responses from backend servers, reducing load and response time.

2.1. Proxy Cache Zone Configuration

# Define cache path and settings (in http context)
http {
proxy_cache_path /var/cache/nginx/proxy
levels=1:2
keys_zone=my_cache:10m
max_size=1g
inactive=60m
use_temp_path=off;

server {
    listen 80;
    server_name example.com;
    
    location / {
        proxy_pass http://backend;
        
        # Enable caching
        proxy_cache my_cache;
        
        # Cache durations
        proxy_cache_valid 200 302 10m;
        proxy_cache_valid 404 1m;
        proxy_cache_valid any 5m;
        
        # Add cache status header
        add_header X-Cache-Status $upstream_cache_status;
    }
}

}

Parameter explanation:

proxy_cache_path /var/cache/nginx/proxy    # Path to store cache
levels=1:2                              # Directory structure (optimizes I/O)
keys_zone=my_cache:10m                  # Zone name and shared memory size
max_size=1g                             # Max cache size on disk
inactive=60m                            # Remove unused cache after 60 minutes
use_temp_path=off;                      # Write directly to cache path

Levels explained:

levels=1:2 creates structure:
/var/cache/nginx/proxy/
├── a/
│   ├── 3c/
│   │   └── abc123...def
│   └── 7f/
└── b/
└── 2d/

First level: 1 character (a, b, c...) Second level: 2 characters (3c, 7f, 2d...)

2.2. Cache Status Values

$upstream_cache_status can be:

  • MISS : Request not in cache, fetched from backend

  • HIT : Request served from cache

  • EXPIRED : Cache entry expired, fetched from backend

  • STALE : Serving stale content (if configured)

  • UPDATING : Cache is updating from backend

  • REVALIDATED: Cache revalidated with backend (304)

  • BYPASS : Cache bypassed (per config)

Logging cache status:

http {
log_format cache_log '$remote_addr - [$time_local] '
'"$request" $status '
'Cache: $upstream_cache_status '
'Time: $upstream_response_time';

server { listen 80; access_log /var/log/nginx/cache.log cache_log;

  location / {
      proxy_pass http://backend;
      proxy_cache my_cache;
      
      add_header X-Cache-Status $upstream_cache_status always;
  }

} }

2.3. Cache Key

The cache key determines the uniqueness of cached items.

Default cache key:

proxy_cache_key $scheme$proxy_host$request_uri;

Example:

http://example.com/page?id=1

Key: httpexample.com/page?id=1

Custom cache keys:

location / {
proxy_pass http://backend;
proxy_cache my_cache;

# Include request method
proxy_cache_key "$scheme$request_method$host$request_uri";

# Include specific headers
# proxy_cache_key "$scheme$host$request_uri$http_accept_language";

# Include cookies
# proxy_cache_key "$scheme$host$request_uri$cookie_session";

}

Example with user-specific caching:

map $cookie_user_id $cache_user {
default $cookie_user_id;
"" "anonymous";
}

server { location /api/ { proxy_pass http://backend; proxy_cache user_cache;

    # Different cache per user
    proxy_cache_key "$scheme$host$request_uri$cache_user";
}

}

2.4. Cache Methods and Conditions

server {
listen 80;

location / {
    proxy_pass http://backend;
    proxy_cache my_cache;
    
    # Only cache GET and HEAD methods
    proxy_cache_methods GET HEAD;
    
    # Cache durations based on status code
    proxy_cache_valid 200 301 302 10m;
    proxy_cache_valid 404 1m;
    proxy_cache_valid any 5m;
    
    # Minimum number of requests before caching
    proxy_cache_min_uses 3;
    
    # Cache even when backend errors
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
    
    # Lock to prevent cache stampede
    proxy_cache_lock on;
    proxy_cache_lock_timeout 5s;
    proxy_cache_lock_age 5s;
}

}

proxy_cache_use_stale explained:

proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;

Serve stale cache when:

- error: Connection error to backend

- timeout: Backend timeout

- updating: Cache is being updated (prevents stampede)

- http_500/502/503/504: Backend returns error codes

2.5. Complete Proxy Cache Example

http {
# Define multiple cache zones
proxy_cache_path /var/cache/nginx/static
levels=1:2
keys_zone=static_cache:10m
max_size=500m
inactive=60m
use_temp_path=off;

proxy_cache_path /var/cache/nginx/api
                 levels=1:2
                 keys_zone=api_cache:10m
                 max_size=200m
                 inactive=10m
                 use_temp_path=off;

upstream backend {
    server backend1.example.com:8080;
    server backend2.example.com:8080;
}

server {
    listen 80;
    server_name example.com;
    
    # Static content cache
    location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
        proxy_pass http://backend;
        proxy_cache static_cache;
        
        proxy_cache_valid 200 30d;
        proxy_cache_valid 404 1h;
        
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_lock on;
        
        expires 30d;
        add_header Cache-Control "public, immutable";
        add_header X-Cache-Status $upstream_cache_status;
    }
    
    # API cache
    location /api/ {
        proxy_pass http://backend;
        proxy_cache api_cache;
        
        proxy_cache_valid 200 5m;
        proxy_cache_valid 404 1m;
        
        proxy_cache_key "$scheme$request_method$host$request_uri$http_authorization";
        proxy_cache_methods GET HEAD;
        proxy_cache_min_uses 2;
        
        proxy_cache_use_stale error timeout updating;
        proxy_cache_lock on;
        
        add_header X-Cache-Status $upstream_cache_status;
    }
    
    # No cache for user-specific content
    location /account/ {
        proxy_pass http://backend;
        proxy_cache off;
        proxy_no_cache 1;
        proxy_cache_bypass 1;
    }
}

}


3. FastCGI Caching

FastCGI caching is used for dynamic content such as PHP applications.

3.1. FastCGI Cache Configuration

http {
# Define FastCGI cache zone
fastcgi_cache_path /var/cache/nginx/fastcgi
levels=1:2
keys_zone=php_cache:10m
max_size=500m
inactive=60m
use_temp_path=off;

# Cache key
fastcgi_cache_key "$scheme$request_method$host$request_uri";

server {
    listen 80;
    server_name example.com;
    root /var/www/html;
    index index.php index.html;
    
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        
        # Enable FastCGI cache
        fastcgi_cache php_cache;
        
        # Cache valid durations
        fastcgi_cache_valid 200 60m;
        fastcgi_cache_valid 404 10m;
        
        # Cache use stale
        fastcgi_cache_use_stale error timeout updating invalid_header http_500;
        
        # Cache locking
        fastcgi_cache_lock on;
        fastcgi_cache_lock_timeout 5s;
        
        # Minimum uses before caching
        fastcgi_cache_min_uses 2;
        
        # Add cache status header
        add_header X-FastCGI-Cache $upstream_cache_status;
    }
}

}

3.2. WordPress with FastCGI Cache

http {
fastcgi_cache_path /var/cache/nginx/wordpress
levels=1:2
keys_zone=wordpress:100m
max_size=1g
inactive=60m
use_temp_path=off;

fastcgi_cache_key "$scheme$request_method$host$request_uri";

server {
    listen 80;
    server_name blog.example.com;
    root /var/www/wordpress;
    index index.php;
    
    # Set cache bypass conditions
    set $skip_cache 0;
    
    # POST requests and URLs with query strings
    if ($request_method = POST) {
        set $skip_cache 1;
    }
    
    if ($query_string != "") {
        set $skip_cache 1;
    }
    
    # Don't cache URIs containing the following segments
    if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") {
        set $skip_cache 1;
    }
    
    # Don't cache logged in users or recent commenters
    if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_logged_in") {
        set $skip_cache 1;
    }
    
    location / {
        try_files $uri $uri/ /index.php?$args;
    }
    
    location ~ \.php$ {
        try_files $uri =404;
        
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        
        # FastCGI cache settings
        fastcgi_cache wordpress;
        fastcgi_cache_valid 200 60m;
        fastcgi_cache_valid 404 10m;
        
        fastcgi_cache_bypass $skip_cache;
        fastcgi_no_cache $skip_cache;
        
        fastcgi_cache_use_stale error timeout updating invalid_header http_500 http_503;
        fastcgi_cache_lock on;
        
        # Headers
        add_header X-FastCGI-Cache $upstream_cache_status;
        add_header Cache-Control "public, max-age=3600";
    }
    
    # Cache static files
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }
    
    # Deny access to hidden files
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
}

}

3.3. Drupal with FastCGI Cache

http {
fastcgi_cache_path /var/cache/nginx/drupal
levels=1:2
keys_zone=drupal:100m
max_size=2g
inactive=60m
use_temp_path=off;

server {
    listen 80;
    server_name drupal.example.com;
    root /var/www/drupal;
    
    # Cache bypass conditions
    set $skip_cache 0;
    
    if ($request_method = POST) {
        set $skip_cache 1;
    }
    
    if ($query_string != "") {
        set $skip_cache 1;
    }
    
    if ($request_uri ~* "^/(admin|user|cart|checkout)/") {
        set $skip_cache 1;
    }
    
    if ($http_cookie ~* "SESS") {
        set $skip_cache 1;
    }
    
    location / {
        try_files $uri /index.php?$query_string;
    }
    
    location ~ '\.php$|^/update.php' {
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_split_path_info ^(.+?\.php)(|/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        
        fastcgi_cache drupal;
        fastcgi_cache_valid 200 301 302 5m;
        fastcgi_cache_valid 404 1m;
        
        fastcgi_cache_bypass $skip_cache;
        fastcgi_no_cache $skip_cache;
        
        fastcgi_cache_use_stale error timeout updating invalid_header http_500;
        fastcgi_cache_lock on;
        
        add_header X-FastCGI-Cache $upstream_cache_status;
    }
    
    location ~ ^/sites/.*/files/styles/ {
        try_files $uri @rewrite;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

}

3.4. Custom PHP Application with Cache

http {
fastcgi_cache_path /var/cache/nginx/app
levels=1:2
keys_zone=app_cache:50m
max_size=1g
inactive=30m
use_temp_path=off;

# Custom cache key with user context
map $http_cookie $user_context {
    default "anonymous";
    ~*user_id=(?<uid>[^;]+) $uid;
}

server {
    listen 80;
    server_name app.example.com;
    root /var/www/app/public;
    
    # Cache bypass for authenticated users
    set $skip_cache 0;
    
    if ($http_cookie ~* "logged_in") {
        set $skip_cache 1;
    }
    
    if ($request_uri ~* "/(api|admin)/") {
        set $skip_cache 1;
    }
    
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        
        # Custom cache key
        fastcgi_cache_key "$scheme$request_method$host$request_uri$user_context";
        
        fastcgi_cache app_cache;
        fastcgi_cache_valid 200 10m;
        fastcgi_cache_valid 404 1m;
        
        fastcgi_cache_bypass $skip_cache;
        fastcgi_no_cache $skip_cache;
        
        fastcgi_cache_use_stale error timeout updating;
        fastcgi_cache_lock on;
        fastcgi_cache_min_uses 3;
        
        # Custom headers
        add_header X-Cache-Status $upstream_cache_status;
        add_header X-Cache-Key "$scheme$request_method$host$request_uri$user_context";
    }
}

}


4. Cache Keys and Cache Zones

4.1. Cache Zones Configuration

http {
# Static content cache - large, long-lived
proxy_cache_path /var/cache/nginx/static
levels=1:2
keys_zone=static:100m
max_size=5g
inactive=7d
use_temp_path=off;

# API cache - smaller, short-lived
proxy_cache_path /var/cache/nginx/api
                 levels=1:2
                 keys_zone=api:50m
                 max_size=1g
                 inactive=1h
                 use_temp_path=off;

# User-specific cache
proxy_cache_path /var/cache/nginx/user
                 levels=1:2
                 keys_zone=user:50m
                 max_size=2g
                 inactive=30m
                 use_temp_path=off;

# HTML pages cache
proxy_cache_path /var/cache/nginx/pages
                 levels=1:2
                 keys_zone=pages:20m
                 max_size=500m
                 inactive=2h
                 use_temp_path=off;

}

Zone size calculation:

keys_zone size stores cache metadata:

  • 1MB ≈ 8000 keys
  • 10MB ≈ 80,000 keys
  • 100MB ≈ 800,000 keys

Max cache size stores actual content:

  • Set based on available disk space
  • Monitor /var/cache/nginx disk usage

4.2. Advanced Cache Keys

Cache by device type:

map $http_user_agent $device {
default desktop;
~mobile mobile;
~tablet tablet;
}

server { location / { proxy_pass http://backend; proxy_cache my_cache;

    # Different cache per device
    proxy_cache_key "$scheme$host$request_uri$device";
    
    add_header X-Device $device;
    add_header X-Cache-Status $upstream_cache_status;
}

}

Cache by accept headers:

map $http_accept $content_type_key {
default "html";
~application/json "json";
~application/xml "xml";
}

server { location /api/ { proxy_pass http://backend; proxy_cache api_cache;

    # Different cache per content type
    proxy_cache_key "$scheme$host$request_uri$content_type_key";
}

}

Cache by language:

map $http_accept_language $lang {
default en;
~^vi vi;
~^ja ja;
~*^zh zh;
}

server { location / { proxy_pass http://backend; proxy_cache my_cache;

    # Different cache per language
    proxy_cache_key "$scheme$host$request_uri$lang";
    
    add_header Content-Language $lang;
}

}

Cache by geo location:

geo $country {
default US;
192.168.1.0/24 VN;
10.0.0.0/8 JP;
}

server { location / { proxy_pass http://backend; proxy_cache my_cache;

    # Different cache per country
    proxy_cache_key "$scheme$host$request_uri$country";
}

}

Complex cache key:

map $http_cookie $user_segment {
default "guest";
~premium=1 "premium";
~vip=1 "vip";
}

server { location /api/ { proxy_pass http://backend; proxy_cache api_cache;

    # Cache key combining multiple factors
    proxy_cache_key "$scheme$host$request_uri$user_segment$device$lang";
    
    add_header X-Cache-Key-Components "segment:$user_segment|device:$device|lang:$lang";
}

}

4.3. Cache Hierarchy

http {
# L1 Cache - Memory (small, fast)
proxy_cache_path /dev/shm/nginx
levels=1
keys_zone=l1_cache:10m
max_size=100m
inactive=5m
use_temp_path=off;

# L2 Cache - SSD (medium, fast)
proxy_cache_path /var/cache/nginx/l2
                 levels=1:2
                 keys_zone=l2_cache:50m
                 max_size=5g
                 inactive=1h
                 use_temp_path=off;

# L3 Cache - HDD (large, slower)
proxy_cache_path /mnt/cache/nginx/l3
                 levels=1:2
                 keys_zone=l3_cache:100m
                 max_size=50g
                 inactive=7d
                 use_temp_path=off;

server {
    location /api/hot/ {
        # Frequently accessed - L1 cache
        proxy_pass http://backend;
        proxy_cache l1_cache;
        proxy_cache_valid 200 5m;
    }
    
    location /api/warm/ {
        # Moderately accessed - L2 cache
        proxy_pass http://backend;
        proxy_cache l2_cache;
        proxy_cache_valid 200 1h;
    }
    
    location /api/cold/ {
        # Rarely accessed - L3 cache
        proxy_pass http://backend;
        proxy_cache l3_cache;
        proxy_cache_valid 200 1d;
    }
}

}


5. Cache Purging and Bypass

5.1. Cache Bypass

server {
listen 80;

# Bypass cache with special header
location / {
    proxy_pass http://backend;
    proxy_cache my_cache;
    
    # Bypass if X-No-Cache header present
    proxy_cache_bypass $http_x_no_cache;
    
    # Or bypass with cookie
    proxy_cache_bypass $cookie_nocache;
    
    # Or bypass with argument
    proxy_cache_bypass $arg_nocache;
}

}

Multiple bypass conditions:

server {
location / {
proxy_pass http://backend;
proxy_cache my_cache;

    # Set bypass conditions
    set $cache_bypass 0;
    
    # Bypass for specific cookies
    if ($http_cookie ~* "admin_logged_in") {
        set $cache_bypass 1;
    }
    
    # Bypass for specific URLs
    if ($request_uri ~* "^/(admin|dashboard)/") {
        set $cache_bypass 1;
    }
    
    # Bypass for POST requests
    if ($request_method = POST) {
        set $cache_bypass 1;
    }
    
    # Apply bypass
    proxy_cache_bypass $cache_bypass;
    proxy_no_cache $cache_bypass;
}

}

Bypass with query parameter:

server {
location / {
proxy_pass http://backend;
proxy_cache my_cache;

    # ?nocache=1 → bypass cache
    proxy_cache_bypass $arg_nocache;
    
    # ?refresh=1 → refresh cache
    proxy_cache_bypass $arg_refresh;
    
    add_header X-Cache-Bypass $arg_nocache;
}

}

Usage:

Normal: http://example.com/page

Bypass: http://example.com/page?nocache=1

Refresh: http://example.com/page?refresh=1

5.2. Cache Purging (Nginx Plus / Third-party module)

Nginx Plus purge:

# Nginx Plus only
map $request_method $purge_method {
PURGE 1;
default 0;
}

server { location / { proxy_pass http://backend; proxy_cache my_cache; proxy_cache_purge $purge_method; } }

Usage:

curl -X PURGE http://example.com/page

Cache purge with open source module:

# Install ngx_cache_purge module

Ubuntu/Debian

sudo apt install libnginx-mod-http-cache-purge

CentOS - compile from source

http {
proxy_cache_path /var/cache/nginx/proxy
levels=1:2
keys_zone=my_cache:10m;

server {
    listen 80;
    
    location / {
        proxy_pass http://backend;
        proxy_cache my_cache;
        proxy_cache_key "$scheme$host$request_uri";
    }
    
    # Purge location
    location ~ /purge(/.*) {
        allow 127.0.0.1;
        allow 10.0.0.0/8;
        deny all;
        
        proxy_cache_purge my_cache "$scheme$host$1";
    }
}

}

Usage:

curl http://example.com/purge/page-to-clear

Manual cache purging:

#!/bin/bash

purge_cache.sh - Manual cache purge script

CACHE_DIR="/var/cache/nginx" CACHE_ZONE="my_cache"

Purge all cache

purge_all() { echo "Purging all cache..." sudo rm -rf ${CACHE_DIR}/${CACHE_ZONE}/* echo "Cache purged!" }

Purge specific URL

purge_url() { local url=$1 local cache_key=$(echo -n "$url" | md5sum | awk '{print $1}') local cache_path=$(find ${CACHE_DIR}/${CACHE_ZONE} -name "${cache_key}")

if [ -n "$cache_path" ]; then
    echo "Purging cache for: $url"
    sudo rm -f $cache_path
    echo "Purged: $cache_path"
else
    echo "No cache found for: $url"
fi

}

Purge by pattern

purge_pattern() { local pattern=$1 echo "Purging cache matching: $pattern" sudo find ${CACHE_DIR}/${CACHE_ZONE} -type f -name "${pattern}" -delete echo "Done!" }

case "$1" in all) purge_all ;; url) purge_url "$2" ;; pattern) purge_pattern "$2" ;; *) echo "Usage: $0 {all|url |pattern }" exit 1 ;; esac

Cache warmup script:

#!/bin/bash

cache_warmup.sh - Warm up cache

URLS=( "http://example.com/" "http://example.com/products" "http://example.com/about" "http://example.com/contact" )

echo "Starting cache warmup..."

for url in "${URLS[@]}"; do echo "Warming up: $url" curl -s -o /dev/null -w "Status: %{http_code}, Time: %{time_total}s\n" "$url" sleep 0.5 done

echo "Cache warmup complete!"

5.3. Selective Cache Invalidation

# API to invalidate cache
server {
listen 80;
server_name example.com;

location / {
    proxy_pass http://backend;
    proxy_cache my_cache;
    proxy_cache_valid 200 10m;
}

# Cache invalidation endpoint
location /api/cache/invalidate {
    allow 127.0.0.1;
    allow 10.0.0.0/8;
    deny all;
    
    content_by_lua_block {
        local pattern = ngx.var.arg_pattern
        if pattern then
            -- Invalidate cache matching pattern
            -- Implementation depends on cache module
            ngx.say("Cache invalidated for pattern: ", pattern)
        else
            ngx.say("Pattern required")
        end
    }
}

}

Usage:

curl "http://example.com/api/cache/invalidate?pattern=/products/*"


6. Practice Exercises

Exercise 1: Browser Caching

  1. Set up a static file server
  2. Configure expires headers:
    • Images: 1 year
    • CSS/JS: 1 month
    • HTML: 1 hour
  3. Test with browser DevTools (Network tab)
  4. Verify cache headers

Exercise 2: Proxy Cache

  1. Set up backend server (Node.js/Python)
  2. Configure Nginx proxy cache
  3. Generate traffic and monitor cache hits/misses
  4. Check cache files in /var/cache/nginx

Exercise 3: FastCGI Cache for WordPress

  1. Install WordPress
  2. Configure FastCGI cache
  3. Test cache bypass for:
    • Logged-in users
    • Admin pages
    • POST requests
  4. Measure performance improvement

Exercise 4: Custom Cache Keys

  1. Set up cache with custom key (include device type)
  2. Test from mobile and desktop
  3. Verify different cached versions

Exercise 5: Cache Purging

  1. Set up proxy cache
  2. Create purge endpoint
  3. Test cache purge:
    • Manual purge script
    • API endpoint
  4. Verify cache cleared

Exercise 6: Cache Performance Testing

  1. Set up with and without cache
  2. Use Apache Bench to benchmark:
# Without cache
ab -n 1000 -c 10 http://example.com/

With cache

ab -n 1000 -c 10 http://example.com/

  1. Compare results

7. Troubleshooting

7.1. Cache Not Working

Problem: X-Cache-Status always shows MISS

Diagnosis:

# Check cache directory permissions
ls -la /var/cache/nginx/

Check cache config

sudo nginx -T | grep cache

Check if cache zone defined

sudo nginx -T | grep keys_zone

Monitor cache files being created

watch -n 1 'ls -lh /var/cache/nginx/proxy/'

Solutions:

# Ensure proper permissions
sudo chown -R nginx:nginx /var/cache/nginx/
sudo chmod -R 755 /var/cache/nginx/

Verify cache zone

http { proxy_cache_path /var/cache/nginx/proxy levels=1:2 keys_zone=my_cache:10m;

server {
    location / {
        proxy_pass http://backend;
        proxy_cache my_cache;  # Must match keys_zone name
        proxy_cache_valid 200 10m;
    }
}

}

7.2. Cache Taking Too Much Disk Space

Problem: /var/cache/nginx fills up disk

Diagnosis:

# Check cache size
du -sh /var/cache/nginx/*

Find largest cache files

find /var/cache/nginx -type f -exec du -h {} + | sort -rh | head -20

Solutions:

# Set max_size
proxy_cache_path /var/cache/nginx/proxy
levels=1:2
keys_zone=my_cache:10m
max_size=1g          # Limit to 1GB
inactive=60m;         # Purge inactive files

Or use tmpfs (RAM disk)

Add to /etc/fstab:

tmpfs /var/cache/nginx tmpfs defaults,size=512M 0 0

7.3. Stale Content Being Served

Problem: Old content is served despite updates

Solutions:

# Clear cache manually
sudo rm -rf /var/cache/nginx/proxy/*
sudo systemctl reload nginx

Or implement cache versioning

location / { proxy_pass http://backend; proxy_cache my_cache;

# Add version to cache key
proxy_cache_key "$scheme$host$request_uri$http_x_app_version";

}