--- name: wordpress-performance-diagnostics description: Use when investigating why a WordPress site in the fleet is slow (frontend TTFB, wp-admin, a REST route, WP-Cron) or before proposing any performance remediation on one. Backend-only, measurement-first diagnostic workflow for headless/jailed WordPress sites reached via `bastille cmd` — establishes baseline evidence (TTFB, autoloaded-options size, object-cache presence, cron health) before any write/remediation action. --- # WordPress Performance Diagnostics Adapted from wordpress/agent-skills' `wp-performance` module (`github.com/wordpress/agent-skills`, `skills/wp-performance`) — identified 2026-08-15 via a paired skills.sh/autoskills.sh catalog scan; both scans independently flagged it as the single best net-new find of the whole scan, since this fleet's WordPress estate is entirely headless/jailed with no convenient browser-first profiling access. This version keeps the upstream's measurement-first structure but replaces its local/SSH assumptions with the fleet's actual `bastille cmd` remote-execution pattern, adds a jail/site selector step (13+ sites across granja/staging/gringo), and narrows the always-collect baseline to four metrics with fleet-specific context for each. ## When to use - A WordPress site (or several) is reported slow — frontend TTFB, wp-admin, a REST route, or WP-Cron. - Before proposing ANY performance remediation on a fleet site — collect the baseline evidence first, always. - Comparing before/after a change (theme deploy, plugin update, object-cache rollout) to confirm it actually helped. Every site in this fleet is a jailed, headless WordPress install with no browser-based profiling access — WP-CLI, `curl`, and log inspection are the only tools available. Treat this as backend-only by default. ## Step 0 — read-only-first, always This skill's job is to produce evidence, not to remediate. Until the user has explicitly asked for a fix (and you've already captured a baseline to measure against), do not: - flush any cache (`wp cache flush`, `wp redis flush`/`wp redis enable`) - install or activate a plugin - change `wp-config.php` or any debug constant (`WP_DEBUG`, `SAVEQUERIES`) - restart php-fpm/nginx/valkey If a fix is later proposed, re-run the exact same measurements from Step 3 afterward and report the delta — see Verification below. Flushing or restarting anything *before* you have a baseline destroys the thing you needed to compare against. ## Step 1 — identify the jail (site selector) The fleet runs 13+ WordPress jails across three hosts. Domain names do not map to jail names by a fixed rule (jail names are typically the domain with punctuation stripped, but always confirm, don't guess) — look it up in the per-host inventory before running anything: - `docs/server-granja.md` — granja's "Site jails" table: 13 WordPress jails, e.g. `menorca.pt` → jail `menorcapt` (`10.20.0.19`), `elrumano.es` → jail `elrumanoro` (`10.20.0.12`). - `docs/server-staging.md` — staging's "Site jails" table, e.g. the `bauxa` jail, which serves Cala Verd restaurant's WordPress site alongside four other, non-WordPress services in the same jail. - `docs/server-gringo.md` — gringo's site jails, e.g. `bauxa-app` (`10.20.0.76`) — the same Cala Verd WordPress site, migrated here 2026-08-13 but not yet cut over in DNS/NPM; both copies are currently live, so confirm which one you actually mean before diagnosing. If a domain isn't in any of those tables, don't guess a jail name — ask, or `ssh root@ "bastille list"` and cross-reference `wp option get siteurl` per jail (the same method used for the 2026-08-13 granja re-audit) rather than trusting an older or stale note. Once you have ``, ``, and the site's on-disk path, confirm the path rather than assuming a fixed convention — e.g. `bastille cmd ls /usr/local/www/`. Multi-service jails (like `bauxa-app`, which serves five separate things behind one jail IP) don't have one obvious WordPress docroot; guessing wrong here silently profiles the wrong document root. ## Step 2 — remote WP-CLI wrapper Reuse the fleet's established remote-execution pattern exactly — see the `wordpress-cli-remote-execution` skill for the full gotcha list (jail-exec output-prefix stripping, `error_reporting(0)` bootstraps swallowing errors, shell globbing failing silently under some remote shells). Do not invent a parallel SSH-direct or local-WP-CLI pattern; every site in this fleet is reached the same way: ```bash ssh root@ "bastille cmd sh -c 'cd && su -m www -c \"wp --path=\"'" ``` - Run as the jail's web user (`www`) via `su -m www -c`, not as root — matches the fleet convention and avoids the `wp-content` ownership-drift problem documented in `wordpress-plugin-conventions` (files created as root over SSH have already caused a real WordPress-core-update failure on this fleet — `menorca.ro`, 2026-07-10). - Always pass `--path=` explicitly, don't rely on `cd` propagating. - Strip the `bastille cmd` wrapper's own `[]:` prefix line before parsing any captured output programmatically. ## Step 3 — the four core baseline metrics Collect all four before proposing any fix, even when the symptom seems to point at one obvious cause — a slow site is frequently more than one thing at once, and an isolated fix without a baseline can't be verified afterward. **1. TTFB** (time to first byte, measured from outside the jail): ```bash curl -s -o /dev/null -w 'ttfb=%{time_starttransfer}s total=%{time_total}s http_code=%{http_code}\n' -H 'Host: ' http:/// ``` Hit the jail's own IP directly with the right `Host:` header (per the `bastille-jail-provisioning` skill's testing gotcha) unless the site's public vhost is already fully wired — hitting the host's `127.0.0.1` with a `Host:` header before that vhost exists silently falls through to a different site's `server_name` and produces a misleadingly "fast" TTFB for the wrong page entirely. Take at least 3 samples and use the median; a single run is noise. **2. Autoloaded-options size:** ```bash ssh root@ "bastille cmd sh -c 'cd && su -m www -c \"wp option list --autoload=on --format=total_bytes --path=\"'" ``` No fixed "too big" threshold is asserted here — compare against the site's own history or a healthy sibling jail rather than trusting one specific byte count as universally bad. To find the biggest individual offenders, add `--fields=option_name,size_bytes` and sort the output. **3. Object-cache presence:** Don't assume every site has a persistent object cache. As of the most recent audit (`docs/server-granja.md`, `docs/server-cerebro.md`), only `menorca.pt`/`menorca.ro` on granja have the Redis Object Cache plugin wired against cerebro's Valkey instance — the other granja jails, and Cala Verd's MariaDB-only setup, do not. Check, don't assume: ```bash ... wp eval 'echo wp_using_ext_object_cache() ? "yes" : "no";' --path= ``` plus confirm the drop-in file exists: `bastille cmd test -f /wp-content/object-cache.php && echo present`. If present, `wp redis status` (Redis Object Cache plugin) reports connection health — Valkey lives on `cerebro`, with per-site ACL credentials; see the Redaction section below before pasting any of that output verbatim anywhere. **4. Cron health:** ```bash ... wp cron event list --path= ... wp cron test --path= ``` Look for: events stuck far in the past (missed spawns), duplicate events on the same hook, and any heavy-looking hook (import/sync/content-agent jobs — several granja sites are fed by hourly RSS/content-agent crons per `docs/server-granja.md`) that might be running inline on a page load instead of via a real WP-Cron spawn. ## Step 4 — deeper diagnostics (only if the four baseline metrics don't explain it) Load [`references/diagnostic-tools.md`](references/diagnostic-tools.md) for `wp doctor`, `wp profile stage/hook/eval`, and headless Query Monitor usage via REST response headers — all still read-only, still WP-CLI/curl based, no browser required. Load [`references/fix-patterns.md`](references/fix-patterns.md) once a dominant bottleneck category is identified (database/queries, autoload, object cache, cron, remote HTTP calls) — condensed fix-pattern guidance per category, still gated by the same read-only-first / measure-before- and-after discipline as everything above. ## Redaction — before pasting ANY of this into a report Diagnostic output on this fleet can surface real secrets. Before quoting command output verbatim in a report, chat message, or committed doc, redact: - `wp config list` / raw `wp-config.php` contents — `DB_PASSWORD`, auth keys/salts, `WP_REDIS_USERNAME`/`WP_REDIS_PASSWORD` (Valkey ACL credentials; per `docs/server-cerebro.md`, per-site passwords live at `/root/.granja_valkey_creds` on cerebro, the admin password at `/root/.valkey_admin_pw` — never paste these values themselves either). - `wp option list` output that happens to include API keys stored as plugin options (payment-gateway keys, mail-relay credentials, etc.). - Any Application Password generated while setting up headless Query Monitor REST auth (see `references/diagnostic-tools.md`). - Full stack traces / DB error output that can echo a connection string. Follow the `` convention used elsewhere in this library (see the `diagnosing-bugs` skill): replace the secret value in place, keep everything else so the diagnostic signal survives. When in doubt whether a value is sensitive, redact it — the cost of over-redacting is a follow-up question; the cost of under-redacting is a leaked credential in a transcript or a committed doc. ## Verification - Baseline and after-fix numbers were captured with the *same* method (Step 3, same site, same `--path`/`--url`, ideally the same time of day — don't compare a cold-cache run to a warm one). - `wp cron test` / `wp doctor check` (if installed) are clean or improved. - No new PHP errors in the jail's PHP-FPM/WordPress debug log after the change. - If the fix required a cache flush, that flush is the *last* step taken, not a diagnostic shortcut — flushing before measuring erases the baseline you needed to compare against. ## Failure modes - **"No change" after a fix** — check you're hitting the same jail IP / domain (`Host:` header) as the baseline, and that PHP's opcache isn't serving a stale compiled version (clearing it, or restarting php-fpm, is itself a diagnostic action worth noting in the report, not a routine step to take silently). - **Noisy TTFB/profiling numbers** — an hourly content-agent cron job on granja sites, or a plugin update check, landing mid-sample can skew a single `curl` run. Take multiple samples. - **Object-cache check says "no" but you expected "yes"** — confirm which site this actually is; only two of granja's 13 jails currently have Redis Object Cache wired up. Check the inventory before treating that as a bug. ## Escalation - Do not install plugins (including the `wp doctor`/`wp profile` packages themselves), enable `SAVEQUERIES`/`WP_DEBUG` in a way that could linger, or flush any shared cache without explicit approval — cerebro's Valkey DB 0 is genuinely shared across sites, and `wp redis enable`'s `FLUSHDB` call can transiently clear a *different* site's cache too (confirmed in `docs/server-cerebro.md`). Treat this as production-adjacent even on a "staging" host — the `staging` host has hosted real production traffic before (see `docs/server-staging.md`). - If system-level profiling (an APM, a PHP profiler extension) is needed beyond WP-CLI's own `profile` package, that's an infra change — flag it rather than installing it unilaterally on a jail. ## Version note Do not assume a fleet-wide WordPress version. The upstream `wp-performance` skill asserts "WordPress 7.0+, PHP 7.4.0+" as its own compatibility target — treat that as the upstream author's target, not a verified fact about this fleet. Confirm per-site with `wp core version --path=`. As one dated data point: granja's 13 WordPress jails were confirmed on core 7.0.4 during the 2026-08-12 fleet-wide security rollout (`docs/server-granja.md`) — that's a specific fact about one host at one point in time, not a floor to assume for staging, gringo, or any future site.