Tuning¶
Profiles¶
| Profile | Use for |
|---|---|
balanced |
a handful of ordinary WordPress sites (default) |
cache |
maximum traffic per gigabyte; the most aggressive cache |
density |
many low-traffic sites on one machine |
database |
WooCommerce, large catalogues, query-heavy workloads |
ngxsetup tune --profile=cache --explain # preview and reasoning
ngxsetup tune --profile=cache --apply --save
--save persists the choice so later commands use it too.
If your sites run heavy page builders, tell the tuner what a worker really costs, and it will size the pool accordingly:
ngxsetup tune --php-worker-mb 160 --explain
Plan for a machine you don't have yet:
ngxsetup tune --memory-mb 16384 --explain
The memory budget¶
tuning.Compute(facts.Facts, Options) Plan is a pure function — no I/O,
no commands run — which is what makes it testable against synthetic
hardware from a 1 GB single-core VPS to a 64 GB 32-core box.
Everything starts here:
total RAM
− reserve for the kernel, sshd, systemd, monitoring
= usable
usable × db_weight → database
usable × php_weight → PHP worker pool
remainder → left free, on purpose
The remainder is not slack. The kernel page cache serves the FastCGI cache and every static asset out of it, so free memory is what turns a cache hit into a memory read. A tuner that allocates 100% of RAM to services produces a slower server than one that does not.
Weights come from the profile. The reserve shrinks proportionally as machines grow, because the base system cost is largely fixed.
Deriving the rest¶
Each number follows from the budget and from what actually constrains it:
- PHP
max_childrenis the smaller ofphp_budget ÷ per_worker_MBandcores × 8. Memory decides how many workers can coexist; CPU decides how many can make progress.tune --explainnames which one bound the result. max_connectionsismax_children + 25. Only PHP workers, wp-cli and admin sessions can open connections, so sizing it from RAM (100 per gigabyte, the previous tuner's approach) creates thousands of slots nothing will use while charging real memory for the buffers behind them.innodb_buffer_pool_sizeis the database budget minus per-connection buffers and fixed overheads, then rounded down to a multiple ofinstances × chunk_size. InnoDB rounds up otherwise, which would push it past its budget.- InnoDB I/O settings come from the detected storage class.
Rotational and solid-state differ by an order of magnitude in the
right
io_capacity; an unknown device defaults to the SSD profile rather than being guessed as rotational, which would cripple fast storage. worker_rlimit_nofileis derived fromworker_connections, so the two always agree.- FastCGI cache max size is 15% of free disk space, rounded to the nearest 256 MB. That rounding matters: free disk drifts on its own between runs (logs, temp files, the cache itself growing) even with zero real configuration change, and an un-rounded value would rewrite the cache config — and reload nginx — on every re-apply for no reason. Found live; see the changelog for the fix.
Flavour awareness¶
MariaDB removed innodb_buffer_pool_instances in 10.5 and refuses to
start if it is present. MySQL 8 removed the query cache.
utf8mb4_0900_ai_ci exists only on MySQL. default_authentication_plugin
was removed in MySQL 8.4. The plan records which flavour is installed
and the template branches accordingly, which is why the same code
produces a working configuration on both.
Caching¶
The FastCGI micro-cache is the main performance mechanism. Three parts matter:
What may be cached is decided by map directives matching the
cookies WordPress and WooCommerce actually set, the request method, and
a list of personal or administrative paths. Notably not excluded:
feeds, sitemaps, paginated archives and URLs with query strings — some
older configurations exclude all of those, which means almost no real
traffic is ever cached.
The cache key normalises away campaign and click-tracking
parameters, so a URL whose query string is nothing but utm_* and
fbclid folds onto its clean form. Every shared social link becomes a
hit instead of a miss.
The serving policy combines fastcgi_cache_lock (a stampede of
concurrent misses for one key collapses into a single upstream request)
with fastcgi_cache_background_update and use_stale (an expired
entry is served immediately while it refreshes behind the scenes).
Together they mean a cache expiry is not a latency spike.
Purging reads the key nginx stores in each cache file's header, so per-site purge works on a stock nginx with no third-party module:
sudo ngxsetup cache purge example.com
sudo ngxsetup cache purge # everything
ngxsetup cache stats
Check whether a request was served from cache via the
X-Cache-Status response header. BYPASS means one of the skip rules
matched — a login cookie, a POST, or an administrative path. MISS
followed by HIT on a second request is correct.
Applying twice changes nothing¶
Rendering is deterministic — no timestamps, no hostnames, no random
values — and every MB-sized value is derived from either total RAM
(essentially constant between two nearby runs) or free disk space,
rounded to absorb ordinary drift. A re-apply with nothing actually
changed reports nothing changed, and reloads nothing — verified in CI
on every push by running tune --apply twice back-to-back against a
real stack. See Architecture → The apply pipeline.