Search by

sympress / nginx-cache

brianvarskonst

Kernel-integrated Nginx edge cache purge package for WordPress.

Package info

github.com/SymPress/nginx-cache

Type:wordpress-plugin

pkg:composer/sympress/nginx-cache

Statistics

Installs: 908

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 2

v1.0.11 2026-10-27 23:03 UTC

README

PHP: ^8.5 License: GPL-2.0-or-later

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-Tag headers 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.