Skip to content

cache-purge: NGINX cache purge and tag invalidation module

Requires the Pro plan (or higher) of the GetPageSpeed NGINX Extras subscription.

Installation

You can install this module in any RHEL-based distribution, including, but not limited to:

  • RedHat Enterprise Linux 7, 8, 9 and 10
  • CentOS 7, 8, 9
  • AlmaLinux 8, 9
  • Rocky Linux 8, 9
  • Amazon Linux 2 and Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install nginx-module-cache-purge
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install nginx-module-cache-purge

Enable the module by adding the following at the top of /etc/nginx/nginx.conf:

load_module modules/ngx_http_cache_purge_module.so;

This document describes nginx-module-cache-purge v2.7.0 released on Oct 13 2026.


Selectively purge content from NGINX's FastCGI, proxy, SCGI, and uWSGI caches using HTTP PURGE requests — no filesystem hacks, no permission headaches.

This is a feature present in NGINX Plus, but ngx_cache_purge brings it to open-source NGINX.

Key Features

  • Same-location purging — add PURGE support directly to existing cache locations, no extra location blocks needed
  • Wildcard purging — purge multiple cache entries with a single request using *
  • Cache tags / surrogate keys — invalidate only objects whose cached response tags match a trusted PURGE pattern
  • Bulk purge — clear all cached content at once with purge_all
  • IP-based access control — restrict who can issue purge requests
  • Cache key method substitution — purge GET-cached content even when $request_method is in your cache key
  • Configurable response format — get purge results in HTML, JSON, XML, or plain text
  • All cache types — works with FastCGI, proxy, SCGI, and uWSGI

Quick Start

Add PURGE support to any cached location:

http {
    proxy_cache_path /var/cache/nginx keys_zone=my_cache:10m max_size=1g;

    server {
        location / {
            proxy_pass        http://127.0.0.1:8000;
            proxy_cache       my_cache;
            proxy_cache_key   "$scheme$host$request_uri";
            proxy_cache_purge PURGE from 127.0.0.1;
        }
    }
}

Purge a cached page:

curl -X PURGE https://example.com/page-to-purge

That's it. No separate /purge location, no filesystem permissions to manage.

Configuration Directives

Enables purging directly in the location that serves cached content.

fastcgi_cache_purge

  • syntax: fastcgi_cache_purge on|off|<method> [purge_all] [from all|<ip> [.. <ip>]]
  • default: none
  • context: http, server, location

proxy_cache_purge

  • syntax: proxy_cache_purge on|off|<method> [purge_all] [from all|<ip> [.. <ip>]]
  • default: none
  • context: http, server, location

scgi_cache_purge

  • syntax: scgi_cache_purge on|off|<method> [purge_all] [from all|<ip> [.. <ip>]]
  • default: none
  • context: http, server, location

uwsgi_cache_purge

  • syntax: uwsgi_cache_purge on|off|<method> [purge_all] [from all|<ip> [.. <ip>]]
  • default: none
  • context: http, server, location

Separate-Location Syntax

Use a dedicated location for purge requests. Useful when you need different access rules for purging.

fastcgi_cache_purge

  • syntax: fastcgi_cache_purge zone_name key
  • context: location

proxy_cache_purge

  • syntax: proxy_cache_purge zone_name key
  • context: location

scgi_cache_purge

  • syntax: scgi_cache_purge zone_name key
  • context: location

uwsgi_cache_purge

  • syntax: uwsgi_cache_purge zone_name key
  • context: location

Response Format

cache_purge_response_type

  • syntax: cache_purge_response_type html|json|xml|text
  • default: html
  • context: http, server, location

Example with JSON responses:

location / {
    proxy_pass        http://backend;
    proxy_cache       my_cache;
    proxy_cache_key   "$uri$is_args$args";
    proxy_cache_purge PURGE from 127.0.0.1;

    cache_purge_response_type json;
}
{"Key": "httplocalhost/"}

Purge Result Contract

Every successful purge response includes a Cache-Purge-Result header. Its value is an HTTP Structured Fields Item whose primary token identifies the operation and whose count parameter reports how many cache objects were invalidated or deleted:

Cache-Purge-Result: exact; count=1
Cache-Purge-Result: wildcard; count=12
Cache-Purge-Result: all; count=47
Cache-Purge-Result: tags; count=3

The operation tokens have these meanings:

  • exact: an exact cache key was invalidated, including a key found through *_cache_purge_key_method substitution. When the cached response carries a Vary header, every stored variant of the key is deleted and count reports the number of cache files removed (the primary entry plus each secondary-key variant).
  • wildcard: cache files matching a key prefix ending in * were deleted.
  • all: cache files in the configured cache zone were deleted by purge_all.
  • tags: cache files whose stored tag header matched the request pattern were deleted. A successful no-match tag purge returns tags; count=0.

The contract is identical for FastCGI, proxy, SCGI, and uWSGI caches because all four backends use the same result-recording and response code. Existing response bodies, content types, and status codes are unchanged.

The header is emitted only after an operation completes successfully. It is absent from authorization failures, invalid tag patterns, exact-key misses, unsupported locations, and internal failures. It is also absent from module versions that predate this contract. Consumers that require a particular operation, such as tag invalidation, must treat an absent header or a different operation token as unsupported rather than inferring success from HTTP 200.

The field has no separate version parameter. Its initial grammar is additive: consumers must ignore unknown parameters, while any incompatible future contract would require a different field name.

Cache Key Method Substitution

When $request_method is part of your cache key, purge requests generate a different key (PURGE vs GET) and won't find the cached entry. These directives solve that:

*_cache_purge_key_method

  • syntax: fastcgi_cache_purge_key_method <method> [<method> ...]
  • context: http, server, location

Available for all cache types: fastcgi_cache_purge_key_method, proxy_cache_purge_key_method, scgi_cache_purge_key_method, uwsgi_cache_purge_key_method.

location ~ \.php$ {
    fastcgi_cache           WORDPRESS;
    fastcgi_cache_key       "$scheme$request_method$host$request_uri";
    fastcgi_cache_purge     PURGE from 127.0.0.1;
    fastcgi_cache_purge_key_method GET;  # Substitutes GET for PURGE in key lookup
}

You can specify multiple methods:

fastcgi_cache_purge_key_method GET HEAD;

Vary Variant Index

cache_purge_variant_index

  • syntax: cache_purge_variant_index <size> | off
  • default: 8m
  • context: http

Since 2.6.2 an exact purge of a key whose cached response carries a Vary header deletes every stored variant. Variant file names are derived from each requesting client's header values, so they cannot be computed from the purge request alone; without extra bookkeeping the module finds them by scanning every file in the cache zone. The scan is correct but costs O(zone size) per Vary'd purge, and the worker is blocked for the duration (measured on a single-worker zone of small files, Linux ext4, Apple M2; "warm" means the page cache already holds the zone):

Files in zone First purge (cold page cache) Repeat purge (warm) Non-Vary purge
10,000 0.5 s 0.03 s 0.2 ms
100,000 4.5 s 0.2 s 0.2 ms
1,000,000 227 s ~1 s 0.1 ms

The variant index removes this cost for repeat purges. It is a shared-memory map from each purged primary key to the variant hashes stored or served for it afterwards, letting the next exact purge unlink exactly those files in O(variants): on the same setup, indexed purges measured 0.2–2 ms regardless of zone size, including the 1,000,000-file zone. Correctness is never traded for speed: a key's first exact purge in an nginx instance's lifetime always scans (and seeds the index), and the index is consulted only while its record is provably complete — a restart, eviction under index memory pressure, or more than 64 variants on one key all drop the record and the next purge falls back to the scan, which re-seeds it. Stale index entries (variant files already evicted by the cache manager) are skipped harmlessly.

The default 8 MB zone tracks tens of thousands of purged keys; size it up for very large purge-heavy deployments, or set off to always use the scan. Non-Vary purges never scan and never touch the index.

The benchmark above is reproducible with bench/variant_scan_bench.sh.

WordPress Integration

FastCGI Cache + Purge

A complete WordPress setup with caching and automatic purge support:

http {
    fastcgi_cache_path /var/cache/nginx levels=1:2
        keys_zone=WORDPRESS:100m max_size=1g
        inactive=60m use_temp_path=off;
    fastcgi_cache_key "$scheme$host$request_uri";

    server {
        listen 80;
        server_name example.com;
        root /var/www/wordpress;
        index index.php;

        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp|avif)$ {
            expires max;
            log_not_found off;
        }

        location / {
            try_files $uri $uri/ /index.php?$args;
            fastcgi_cache_purge PURGE from 127.0.0.1;
        }

        location ~ \.php$ {
            try_files $uri =404;
            fastcgi_pass unix:/run/php-fpm/www.sock;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;

            fastcgi_cache WORDPRESS;
            fastcgi_cache_valid 200 60m;
            fastcgi_cache_use_stale error timeout updating;
            fastcgi_cache_lock on;
            fastcgi_cache_purge PURGE from 127.0.0.1;

            add_header X-Cache-Status $upstream_cache_status always;
        }
    }
}

Why fastcgi_cache_purge in both locations? The root URL / is a special case. When try_files checks $uri/, it finds the document root directory. For GET requests, the index directive routes to index.php — but for PURGE requests, index doesn't apply and NGINX stops at location /. Adding fastcgi_cache_purge there ensures PURGE / works.

Proxy Cache Purge Plugin

Install the Proxy Cache Purge plugin to automatically purge cache when content is updated:

wp plugin install varnish-http-purge --activate
wp option update vhp_varnish_ip '127.0.0.1'

The plugin sends PURGE requests to NGINX whenever posts, comments, or pages are modified — no manual cache management needed.

Cache Key Best Practices

Keep it simple — avoid $request_method in keys

# Recommended
fastcgi_cache_key "$scheme$host$request_uri";

# Avoid — purge requests won't match cached GET entries
fastcgi_cache_key "$scheme$request_method$host$request_uri";

If you must include $request_method, use *_cache_purge_key_method GET to fix purge key lookups.

Wildcard Purging

Purge multiple entries matching a pattern by appending *:

curl -X PURGE https://example.com/blog/*

The asterisk must be the last character. For this to work, $uri must be at the end of your cache key.

Cache Tags / Surrogate Keys

cache_purge_tags connects a tag header stored in cached upstream responses to a regex header on an authorized PURGE request:

location / {
    proxy_pass         http://backend;
    proxy_cache        app_cache;
    proxy_cache_key    "$scheme$host$request_uri";

    proxy_cache_purge  PURGE from 127.0.0.1;
    cache_purge_tags   X-Magento-Tags X-Magento-Tags-Pattern;
}
  • syntax: cache_purge_tags <cached-response-header> <purge-pattern-header>
  • default: off
  • context: http, server, location

For Magento Open Source and Adobe Commerce, no application change is needed. Magento already adds X-Magento-Tags to cacheable responses and sends trusted localhost PURGE requests with X-Magento-Tags-Pattern. The module scans the same cache zone and removes only matching objects. A tag pattern takes precedence over exact-key, wildcard, and purge_all behavior for that request.

The response tag header may be hidden from clients with proxy_hide_header. It remains available in NGINX's on-disk cache metadata for invalidation. An invalid regex returns HTTP 400; a valid pattern with no matches returns HTTP 200, matching Magento's generated Varnish VCL.

Tag invalidation scans cached header metadata only when a PURGE arrives. Normal cache hits pay no tag-index overhead, but purge time grows with the number of files in the cache zone. This trades Varnish's persistent ban list for immediate deletion of matching NGINX objects.

WordPress cache-tag integrations can use the same directive with their header names:

cache_purge_tags X-Cache-Tags X-Cache-Tags-Pattern;

Bulk Purge

Clear all cached files at once:

proxy_cache_purge PURGE purge_all from 127.0.0.1;

This can be slow with large caches or slow storage. Use RAM-backed cache paths for best performance.

IP-Based Access Control

Restrict purge requests to trusted sources:

fastcgi_cache_purge PURGE from 127.0.0.1 192.168.1.0/24;

Troubleshooting

Response Cause Fix
405 Not Allowed PURGE hit a location without *_cache_purge Add *_cache_purge to all relevant locations
412 Precondition Failed Cache entry not found (never cached, already expired, or key mismatch) Check cache key — look for $request_method issues. On releases before 2.6.2, an upstream Vary header (e.g. Vary: Accept-Encoding) also caused 412 unless the purge request carried the exact same header value as the cached variant; upgrade, or work around with proxy_ignore_headers Vary
403 Forbidden Client IP not in from list Add your IP to from
200 OK but cache persists $request_method in cache key creates mismatched keys Remove $request_method from key, or add *_cache_purge_key_method GET

Vary / gzip_vary interaction: When the upstream response carries a Vary header (for example Vary: Accept-Encoding, or nginx's own gzip_vary), nginx stores one variant per distinct request header value under the same cache key. Since 2.6.2 an exact purge deletes every stored variant of the key and reports the number of files removed in Cache-Purge-Result: exact; count=N; since 2.7.0 repeat purges of a key use the variant index instead of scanning the cache zone. On older releases an exact purge only removed the variant matching the purging request's own headers (usually returning 412); there, work around it with proxy_ignore_headers Vary in the cached location, or disable gzip_vary.

Testing

ngx_cache_purge includes a test suite based on Test::Nginx:

prove

See Also