sympress / nginx-cache
Kernel-integrated Nginx edge cache purge package for WordPress.
Package info
github.com/SymPress/nginx-cache
Type:wordpress-plugin
pkg:composer/sympress/nginx-cache
Requires
- php: ^8.5
- ext-dom: *
- ext-sodium: *
- predis/predis: ^3.6
- symfony/clock: ^8.0
- symfony/console: ^8.0
- symfony/filesystem: ^8.0
- symfony/finder: ^8.0
- symfony/http-client: ^8.0
- symfony/lock: ^8.0
- symfony/options-resolver: ^8.0
- sympress/event-dispatcher: ^1.0.2
- sympress/kernel: ^1.1.1
- sympress/wp-cli-console: ^1.0.2
Requires (Dev)
- symfony/var-dumper: ^8.1
- sympress/qa: ^0.1.2
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-28 13:06:04 UTC
README
SymPress Nginx Cache provides WordPress cache purge controls for Nginx FastCGI, proxy and uWSGI cache setups running on the SymPress kernel. It combines admin tools, WP-CLI commands, automatic purge hooks, surrogate tag tracking, queue processing, diagnostics and generated Nginx configuration.
Version 1 adds safe Multisite scope, Network Admin, delegated editor actions, a network worker, migration, Site Health, compact setup, scalable tag retention and Polylang integration. See operation and configuration, migration and the extension API.
Package
composer require sympress/nginx-cache
The package is discoverable by sympress/kernel through Composer metadata:
{
"extra": {
"installer-name": "ns-nginx-cache",
"kernel": {
"bundle": "SymPress\\NginxCache\\NginxCacheBundle",
"entry": "ns-nginx-cache/ns-nginx-cache.php"
}
}
}
The display name remains Nginx Cache. The WordPress plugin slug is
ns-nginx-cache; release archives use the same
folder and ns-nginx-cache.php entry file. The Composer package remains
sympress/nginx-cache. Its installer-name sets the folder when using
composer/installers.
The Update URI header
identifies this GitHub-distributed plugin and prevents WordPress.org from
offering updates for an unrelated plugin, including while this plugin is inactive.
This header does not install a GitHub updater; updates still use Composer or
the release archive. See upgrading an existing installation
before switching from the old nginx-cache folder.
WP-CLI
wp nginx-cache status wp nginx-cache purge / wp nginx-cache purge --queue --prewarm wp nginx-cache diagnostics wp nginx-cache config wp nginx-cache queue details wp nginx-cache side-effects details
The legacy edge-cache namespace is also registered for compatibility.
Queues retain failed work, retry at most five times with capped exponential
backoff, and expose exhausted work through status and details. After fixing
the cause, wp nginx-cache queue retry or wp nginx-cache side-effects retry
resets its retry budget and schedules processing. See the
side-effect contract for timing and delivery guarantees.
New purge events use 64 bounded merge slots plus at most four overflow markers for the dry-run/prewarm combinations. When selective work exceeds the URL budget, it becomes a full invalidation within its original scope rather than losing URLs. URL-only editor/REST requests retain selective targets and fail explicitly if the durable inbox is unavailable; they never escalate to a site purge. Retry exhaustion does not scan the inbox on content hooks. Existing legacy inbox rows are ingested in batches of at most 68 after processing or an explicit retry resumes.
Configure Nginx caching
Cache directory does not exist. means the plugin's configured cache path is
missing. Installing the plugin alone does not configure Nginx. Apply the HTTP
and FastCGI sections generated by the package, using the same path for Nginx and
the plugin. Nginx creates the cache directory when it loads fastcgi_cache_path;
its worker user and PHP must both be able to read and write that directory.
Generate separate includes with wp nginx-cache config --section=http and
wp nginx-cache config --section=fastcgi. The default --section=all output
remains a reference containing sections for multiple contexts; it cannot be
included as a single Nginx configuration file. --section=server exports the
static-asset location and metrics logging directive. --section=logging
exports only the metrics logging directive. These same options work through
wp console nginx-cache:config.
Regenerate the HTTP and FastCGI includes together after updating the plugin:
the FastCGI include references maps defined in the HTTP include, and nginx -t
rejects a mixed pair. Use Nginx 1.23 or newer. Older versions only expose the
first of repeated upstream Cache-Control headers, so a private directive in
a later header would not keep a response with a positive X-Accel-Expires out
of the cache.
For a project-owned DDEV Nginx configuration:
mkdir -p .ddev/nginx-cache ddev exec --raw -- wp nginx-cache config --section=http > .ddev/nginx-cache/http.conf ddev exec --raw -- wp nginx-cache config --section=fastcgi > .ddev/nginx-cache/fastcgi.conf
Add include /mnt/ddev_config/nginx-cache/http.conf; above the server block
in .ddev/nginx_full/nginx-site.conf. Inside the existing PHP location, add
include /mnt/ddev_config/nginx-cache/fastcgi.conf; after
include fastcgi_params;. Remove the #ddev-generated marker if present, then
run ddev restart. Ensure generated files contain only Nginx directives; PHP
diagnostics from an outdated WP-CLI must not be redirected into them.
Regenerate both includes after changing cache profiles or bypass rules.
Repeat existing server-level add_header directives in the PHP location after
the include, for example add_header X-Content-Type-Options nosniff always;.
The cache status headers otherwise replace Nginx's normal header inheritance.
See DDEV's Nginx customization
and the NGINX FastCGI cache directives.
Verify X-Nginx-Cache: MISS followed by HIT on an anonymous page. Admin,
login, authenticated requests, semantic queries and non-GET/HEAD methods must
avoid cache hits. Eligible GET/HEAD requests report BYPASS; other methods may
omit X-Nginx-Cache because they never enter the cache handler.
Do not suppress the directory warning or mark an arbitrary
directory as a cache root merely to make the dashboard green.
Regenerate older configurations: the empty-query allowlist must use the exact
"" 0; map entry because Nginx skips regex matching for empty map inputs.
Run composer tests:nginx with a real nginx binary to verify directory creation
and query/authentication bypass maps against an isolated local Nginx instance.
Dashboard metrics
The dashboard leads with page coverage: public page URLs (home page, language home pages and published content of viewable post types, every language version as its own URL) are compared with the local cache files on each reload. Pages are counted as cached, expired (file present, validity passed; Nginx refreshes it on the next request), not cached, intentionally excluded by a bypass rule, or unknown (unparsable file). Key variants of one URL count once. At most 500 recently modified entries are checked and a limited check is labelled; an unreadable cache root or an alternative purge backend shows "not determinable" instead of zero. The check reads only the filesystem: no HTTP request, purge or prewarm.
The HTTP hit rate lives in the Tools tab (in simple mode under "Technische
Statistik"). It counts individual HTTP requests, not distinct pages or visitors,
over a sliding window. Its panel combines the hit rate, its request denominator and a
proportional cache-served/newly-requested split. File, size, queue and tag counts
remain alongside it. Measurement details are initially collapsed and contain
all six Nginx status counts, eligibility rules and sample limitations. Native
keyboard controls open the details without submitting the settings form.
The plugin uses NGINX green accents and black action buttons, independently of
the surrounding WordPress admin theme. Card headings share one type style;
section descriptions have explicit spacing beneath their titles.
The purge-rule matrix scrolls within the Cache section on narrow screens and
can be focused and scrolled by keyboard. Section buttons expose their active
state with aria-pressed.
The HTML prototype provides preview states;
its example data and state selector are not production measurements or settings.
Cache file count and size inspect the configured cache root (excluding the
plugin lock and sentinel); queue count and tag/URL counts query their persistent
stores. Counts can be zero when no anonymous page is cached. A bounded filesystem
scan displays a + after both file count and size when it is incomplete.
Unavailable roots show an unavailable value instead of a misleading zero. The
header confirms the cache directory's readiness, not that Nginx served a HIT.
The hit rate reads real Nginx requests, including hits that never execute PHP.
Apply the HTTP include (which defines the JSON log format) and export
wp nginx-cache config --section=logging into a separate include. Add that
include alongside existing access_log directives in the server block;
location-specific access_log overrides must also include it to count those
requests. Existing access logging must be preserved.
The default log is /var/log/nginx/sympress-cache-metrics.jsonl. Override it with
the private SYMPRESS_NGINX_CACHE_METRICS_LOG PHP constant and regenerate the
includes. Keep the absolute path outside the cache root and document root, grant
PHP read access, and configure normal log rotation. The reader also reads the
previous generation <log>.1 within the same 1 MiB / 5,000-line budget, so both
copytruncate and rename/create rotation keep current records; bytes still shared
by both files during copytruncate are counted once. Keep delaycompress when
compressing: an unread compressed or older generation written within the window
marks the measurement window as incomplete. Nginx writes only timestamp,
host and cache status, without IPs, URLs, cookies or authorization headers.
See the NGINX access log directives.
The card covers this site's host over the last 60 minutes. HIT, STALE, UPDATING and REVALIDATED count as cache-served responses; MISS and EXPIRED count as misses. BYPASS and non-cache requests are excluded. Reads are limited to the last 1 MiB and 5,000 records and labelled as a sample when capped. Missing/unreadable logs or no eligible samples show no measurement, never an invented 100%. The card distinguishes missing/unreadable logs from a readable log with no eligible requests in the current window. Dashboard values are a snapshot taken when the page loads; reload after generating anonymous traffic. The dashboard performs no HTTP probes, log writes or purge operations. One shared diagnostic snapshot supplies the page, avoiding duplicate filesystem scans, configuration generation, history and queue reads. The details show each cache status separately, the snapshot time, and a small-sample notice below 100 eligible requests. Prewarm requests are included in the percentage.
Cache limits and background prewarm
The advanced settings export includes editable settings from every tab. Import fills the complete form; save once to apply the imported values together. Secrets and their clear actions are excluded, so target-site credentials remain unchanged. Review cache paths and URLs for the target environment before importing. Settings are stored in each site's database; deploying plugin code does not transfer them between environments.
The Cache tab configures these generated Nginx limits. All profiles preserve the same defaults; changing a profile does not silently increase cache lifetime.
Setting / SYMPRESS_NGINX_CACHE_ constant suffix |
Default | Allowed range |
|---|---|---|
valid_seconds / VALID_SECONDS |
600 seconds | 60–86400 seconds |
inactive_seconds / INACTIVE_SECONDS |
3600 seconds | 60–604800 seconds |
retention_mode / RETENTION_MODE |
keep_warm |
keep_warm, evict_inactive |
max_size_mb / MAX_SIZE_MB |
256 MiB | 16–65536 MiB |
keys_zone_mb / KEYS_ZONE_MB |
100 MiB | 1–1024 MiB |
Options use the sympress_nginx_cache_ prefix. Constants override saved options.
Malformed values fall back to defaults and numeric values are bounded. Export,
review and apply both HTTP and FastCGI includes, then run nginx -t before
reloading Nginx. Saving plugin options alone does not alter the running server.
Upstream cache headers can override validity; inactive can evict an entry even
when its response remains fresh. Only extend validity with reliable automatic
invalidation. See the NGINX FastCGI cache directives.
Three separate mechanisms decide what a visitor receives:
- Freshness (
valid_seconds): how long a cached response is served as fresh. Afterwards Nginx serves it stale once and refreshes it in the background. - Inactivity eviction (
inactive_seconds): Nginx removes entries that were not requested within this window, even when they are still fresh. - Storage pressure (
max_size_mb): when the zone is full, Nginx evicts the least recently used entries regardless of the other two settings.
retention_mode controls prewarmed pages. With the default keep_warm, a bounded
background job requests the prewarm targets every half inactivity window
(1800 seconds by default, at least 60), five URLs per cron tick. Each request
renews the entry's inactivity timer, so successfully prewarmed, cacheable targets
stay available during idle periods while purges, content updates, bypass rules
and storage limits still apply. It needs prewarm enabled and the per-minute
cron runner below; with a timeout under about ten minutes, cron granularity may
not keep up. Failures and overdue cycles appear in wp nginx-cache diagnostics
(keep_warm) and Site Health; a first cycle that never completes is overdue one
inactivity window after it started or became due. Choose evict_inactive for large sites with many
rarely visited URLs or limited storage: no refresh runs and prewarmed pages are
evicted like any other entry. Nginx has no inactive=off; neither mode makes
retention indefinite, and other cached pages always follow inactive_seconds.
Upgrades keep an explicit choice: without a saved retention_mode, a site whose
stored inactive_seconds or INACTIVE_SECONDS constant differs from 3600
continues with evict_inactive; sites on the previous default switch to
keep_warm. Saving the Cache tab stores the shown mode.
Enable automatic purge, selective purge, the queue and prewarm together for
targeted invalidation followed by warming affected pages. Each side-effect tick
handles at most one task and five prewarm URLs. Target discovery is checkpointed
once; each successful URL is checkpointed individually. Successful continuations
are scheduled again without spending the five-failure retry budget. Same-origin
redirects are followed for at most three hops, revalidating every hop. Outdated
targets (other 3xx/4xx responses, external redirects, loops, longer chains or a
changed site origin) are skipped and settled so they cannot block later targets;
they are never counted as warmed. Timeouts, 408/425/429 and 5xx responses remain
retryable within the budget; acknowledged URLs and other providers are not
repeated. The last 20 prewarm issues (URL, HTTP status or safe reason, time) appear
in wp nginx-cache side-effects details and the diagnostic report. Delivery
remains at least once around a crash before a checkpoint. Same-origin checks and
five-second request limits remain enforced. Initial opt-in sitemap discovery
retains its separate bounded 20-map budget. If discovery fails, already known
targets (fixed prewarm URLs, the home page and entries of maps that did load) are
still warmed and later cache layer, remote and Cloudflare work still runs; only
then does the missing discovery spend one retry, and a later successful discovery
adds just the missing targets.
The last discovery failure (sitemap URL and fixed reason: HTTP status, timeout,
request failure, size or XML) appears as sitemap_discovery_failure in the same
reports and is cleared by the next successful discovery. Explicit CLI prewarm remains
synchronous and reports failed or skipped targets with a nonzero exit code.
Run wp cron event run --due-now once per minute through an external scheduler
under the site's normal user. Cached visitors do not execute WordPress and cannot
be relied on to drain queues. Once that runner is verified, configure
DISABLE_WP_CRON=true to avoid visitor-driven scheduling. Monitor pending and
exhausted tasks via wp nginx-cache queue and wp nginx-cache side-effects.
See WordPress system scheduling.
Dry runs also work when Nginx has expired every cache file and retained only its empty hexadecimal directories. A dry run creates no cache directory, sentinel or lock file and deletes nothing. It respects an existing purge lock and previews a snapshot when no lock exists. The notice reports the number of top-level entries that a full purge would remove; cache-file KPIs count files inside those entries. Failure notices include the actual cause rather than only a generic purge error.
Features
WordPress admin notices appear below the plugin header, outside the onboarding banner and settings cards. A missing cache-directory warning means the configured local cache path is unavailable; installing the plugin does not enable Nginx FastCGI caching or create that directory.
-
Select Redis page-cache or Nginx
GET /purge/<path>backends instead of local files. -
Configure separate homepage, singular-page and archive rules for edits, deletes and comment approval/removal.
-
Discover prewarm URLs from same-origin sitemap indexes or URL sets.
-
Generate and optionally atomically maintain a Multisite Nginx map.
-
Optionally include a rendering timestamp, query count and duration in public HTML.
-
Purge Nginx cache files by URL, path, cache layer or full cache directory.
-
Queue purge requests and process side effects safely.
-
Track surrogate tags for targeted invalidation.
-
Purge affected posts, posts pages, feeds, date archives, paginated archives, AMP companion URLs, REST/GraphQL resources and WooCommerce product URLs.
-
Forward tag purges to Cloudflare through
Cache-Tagheaders and the Cloudflare purge API when configured. -
Delegate whole-zone purges to a protected Nginx endpoint instead of removing local cache files directly.
-
Inspect cache path availability, writability, file counts and byte size.
-
Generate Nginx cache snippets for the configured profile.
-
Prewarm selected URLs after purge operations.
-
Expose admin dashboard actions and REST endpoints for integrations.
Tested scale
Run the full benchmarks locally from a repository checkout:
./bin/run-scale.sh
The runner requires Bash and Docker Engine; PHP, Composer and a local WordPress installation are not required on the host. It prepares its own PHP 8.5 environment, Composer dependencies and checksum-verified WordPress 7.1.2, then runs the unchanged benchmarks sequentially on MariaDB and MySQL. Each database gets a fixed 512 MiB InnoDB buffer pool with its normal durability settings. The report records effective memory, durability settings and the ten slowest data queries.
Each invocation writes separate engine reports and logs under
build/scale/<run-id>/<engine>/, including scale-report.json. It exits successfully
only when both selected engine runs pass. The default overall timeout is 3,600
seconds, including setup. Use ./bin/run-scale.sh --engine=mariadb or
--engine=mysql to select one engine, --timeout=SECONDS to change the timeout,
and --help for usage.
The source checkout is mounted read-only and copied into a private container; host dependencies and lock files are preserved. Databases use a dedicated Docker network without publishing host ports. The runner removes its own containers, database volumes and network on completion, failure or interruption, while retaining reports and logs. Coding agents should inspect the command's exit status and the reports from that invocation. These benchmarks are no longer part of GitHub Actions.
composer tests:scale remains the lower-level PHP entry point for a separately
prepared disposable database/core environment using the integration harness's
environment variables. The harness itself never changes database server settings.
Measured locally on 2026-10-09: native Linux, PHP 8.5.4, WordPress 7.1.2, MariaDB 11.8.6 with a 64 MiB buffer pool; warm filesystem cache, zero external HTTP. Fixture seeding is excluded. Nginx entries use synthetic lightweight bodies; network fixtures have real site records and only the options tables used by the worker. Separate native Nginx tests verify real binary KEY parsing and MISS/HITs.
| Workload | Measurement |
|---|---|
| 50,000 posts, 200 categories; 200 content-hook samples | 9.3 ms save_post p95 |
| 10,000 cache files | 37,179 files/s site scan |
| 200 queued edits coalesced to one purge | 145.8 coalesced tasks/min (one measured task, not a capacity bound) |
| 100,000 indexed URLs / 200,000 rows | 159.0 ms maximum query; limits reached in 398 batches of at most 500 rows |
| Shared root, 500 sites × 200 files | 4.5 s full site purge, retaining all other sites |
| 500 pending site URL tasks | 9.2 s network worker |
| 5,000 registered sites | 27.0 ms for the current 20-row Network Admin page |
These measurements describe this fixture/environment, not production latency
guarantees. Provider requests and real page rendering add their own cost.
The report preserves maximum index data-query, transaction and overall statement
latency separately. The 200 ms index target applies to data queries. On the hosted
MySQL runner, one COMMIT took 330.9 ms (MariaDB: 690.7 ms); storage/fsync and scheduling can exceed
that target even when index queries meet it. No durability setting is relaxed.
Earlier default-sized MySQL runs also recorded isolated insert/delete spikes
of 303/413 ms and one 98.9 ms hook p95. The index alone allocated about 136 MB,
exceeding MySQL's 128 MiB default buffer pool.
Memory pressure is a possible contributor; these results remain limitations,
not a universal 200 ms latency guarantee. The local runner uses the explicit memory
budget above, with the same latency targets and full durability.
Nginx Helper compatibility
The Cache settings tab contains backend connection settings and the purge-rule
matrix. Existing installations continue using local files and their existing
all-scope rules. Redis and HTTP are alternatives to disk purging, not WordPress
object-cache adapters. Redis requires a site-dedicated, nonempty prefix; passwords
are encrypted and are never exported or rendered. TCP, ACL users and Unix sockets
are supported. Redis keys use either PREFIX$scheme$request_method$host$request_uri
(nginx-srcache / Nginx Helper) or SymPress's pipe-separated key format. Coordinate
the key and prefix with your server configuration. A full Redis purge removes
only that prefix. It never runs FLUSHDB/FLUSHALL.
For HTTP URL purges configure the same-origin prefix (default /purge) and the
existing remote signing secret. A compatible FastCGI/proxy purge module must
provide that location. Protect it with an IP allowlist or signature verification;
the module does not itself authenticate signature headers. The existing protected
full-purge endpoint is required for full HTTP purges. No redirects are followed.
Enable both prewarm and sitemap discovery in the Preload tab, which holds
all preload settings, to preload after full purges, or run
wp nginx-cache prewarm. The default sitemap is /wp-sitemap.xml; custom SEO
sitemap indexes can be configured. Discovery reads at most 20 sitemaps (2 MiB
each) and shares the existing configurable limit of up to 200 prewarm URLs.
Foreign-origin entries, external entities and redirects are rejected. Selective
purges continue warming only their affected URLs. The optional HTML stamp is
restricted to anonymous HTML responses; REST, feeds, AJAX, cron and login/admin
responses are unchanged. It records PHP rendering time, not a claim of a cache hit.
Network administrators can preview the Multisite map. For automatic persistence,
set SYMPRESS_NGINX_CACHE_MULTISITE_MAP_FILE to an absolute .conf filename outside
the public document root. Site creation/update/deletion and admin initialization
refresh that file atomically, leaving unchanged content and previous files on
failure intact. Nginx must include the map in its HTTP context and use
$sympress_blog_id in the appropriate static-upload rule. Nginx configuration,
legacy/current upload paths and reloads remain operator responsibilities. Up to
5000 active sites per current network are supported; domain aliases can be added
with sympress_nginx_cache_multisite_map_entries. For multiple networks configure
separate destinations per network in wp-config. WordPress itself detects Nginx
rewrite support; no additional index.php permalink workaround is installed.