How to Add No-Cache Headers to 404 Pages in Apache and Nginx to Prevent Cloudflare Caching Issues


2 views

When using Cloudflare in front of a load-balanced environment, cached 404 responses can create significant operational headaches. Here's the core issue:

  • Cloudflare caches responses based on headers
  • Neither Apache nor Nginx send no-cache headers for 404s by default
  • In dynamic environments using rsync/lsyncd, 404s may be temporary
  • Cached 404s persist even after the backend file becomes available

For Apache (2.4+), add these directives to your main configuration or virtual host:

<IfModule mod_headers.c>
    Header set Cache-Control "no-cache, no-store, must-revalidate" env=ERROR404
    Header set Pragma "no-cache" env=ERROR404
    Header set Expires "0" env=ERROR404
</IfModule>

ErrorDocument 404 /404.html
ErrorDocument 403 /404.html

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule .* - [E=ERROR404:1]

For Nginx, modify your server block configuration:

server {
    # ... other config ...
    
    error_page 404 /404.html;
    location = /404.html {
        internal;
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        add_header Pragma "no-cache";
        add_header Expires "0";
    }

    location / {
        try_files $uri $uri/ =404;
    }
}

Verify the headers using curl:

curl -I https://yourdomain.com/nonexistent-file.html

You should see these headers in the response:

HTTP/1.1 404 Not Found
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0

For WordPress-specific setups, you might need to add this to wp-config.php:

function no_cache_404() {
    if (is_404()) {
        nocache_headers();
    }
}
add_action('template_redirect', 'no_cache_404');

For environments using both Apache and Nginx (like reverse proxy setups), ensure the headers aren't being stripped at any layer.


After migrating to Cloudflare, I noticed an unexpected behavior: Cloudflare was caching 404 error pages. This became problematic in our multi-server environment where 404s occasionally occur during brief synchronization gaps (handled by lsyncd/rsync). Without proper cache-control headers, Cloudflare would serve the cached 404 responses for extended periods, even after the underlying resource became available.

Neither Apache nor Nginx sends Cache-Control: no-store or similar headers by default for 404 responses. This makes sense for most static sites, but becomes problematic when using reverse proxies like Cloudflare that aggressively cache responses based on headers.

For Apache (2.4+), add this to your global configuration or virtual host:


<IfModule mod_headers.c>
    Header set Cache-Control "no-store, must-revalidate" env=ERROR_404
    Header set Pragma "no-cache" env=ERROR_404
    Header set Expires "0" env=ERROR_404
</IfModule>

ErrorDocument 404 /error/404.html
RewriteEngine On
RewriteCond %{REQUEST_URI} ^/error/404\.html$
RewriteRule .* - [E=ERROR_404:1]

For Nginx, add this to your server block:


location / {
    error_page 404 /404.html;
    location = /404.html {
        add_header Cache-Control "no-store, must-revalidate";
        add_header Pragma "no-cache";
        add_header Expires "0";
        internal;
    }
}

Verify the headers using curl:


curl -I https://yourdomain.com/nonexistent-page

You should see headers like:


HTTP/2 404
cache-control: no-store, must-revalidate
pragma: no-cache
expires: 0

For Cloudflare, you might also want to:

  1. Create a Page Rule matching your error pages with Cache Level = "Bypass"
  2. Set Cloudflare's "Browser Cache TTL" to "Respect Existing Headers"