← Blog

WordPress

WordPress multisite audit: chat-agent retrofit checklist

A Utrecht heating group asks for a chat agent on three subsites by August. Before we quote, the audit checklist runs over their WordPress multisite.

Jacob Molkenboer· Founder · A Brand New Company· 30 Nov 2025· 6 min
Open leather logbook with ivory tabs, brass key on spine, green sticky note, red wax seal on bone paper.

The marketing lead at a Utrecht heating-systems group emails on a Tuesday in May. They want a chat agent on three of their fifteen subsites by August. They mention, almost as a footnote, that the network runs on WordPress 6.4 multisite, has been live since 2017, and "should be fine to drop something into." Before we reply with a price, the audit checklist runs. The checklist exists because chat agents that get bolted onto a slow multisite turn into the reason the homepage starts timing out at 2pm.

Here is what we score, in the order we score it, and why that order matters.

Opening wp_options first

A chat agent is a tiny client. It opens a session, posts to a REST endpoint, gets a reply, closes. The widget itself weighs maybe 40KB gzipped. None of that is the problem. The problem is that every REST request to a WordPress instance loads autoloaded options into memory before it touches the route. If autoload weighs 6MB, a three-round chat turn just paid for 18MB of option queries before producing any useful work.

The query is one line per subsite:

SELECT SUM(LENGTH(option_value)) / 1024 / 1024 AS autoload_mb
FROM wp_2_options
WHERE autoload = 'yes';

We run this across every wp_N_options table in the network, then drop the biggest offenders:

SELECT option_name, LENGTH(option_value) AS bytes
FROM wp_2_options
WHERE autoload = 'yes'
ORDER BY bytes DESC
LIMIT 15;

The 4MB autoload red line

The thresholds we use, against fifty or so Dutch SME multisites in the last two years:

  • Below 1MB: healthy. Ship the agent.
  • 1 to 3MB: monitor. Add a Query Monitor pass after launch.
  • 3 to 4MB: clean up first. The agent will work, but the page that hosts it will lose 200 to 400ms.
  • Above 4MB: red line. Refuse to quote until the table is reduced.

The 4MB number is not arbitrary. Above that, PHP's serialize and unserialize work on every request starts costing more than the chat agent's actual round trip to the LLM. You end up debugging a "slow chatbot" that is actually a slow site.

Warning

Never bulk-delete autoloaded options without a snapshot. Yoast Indexable, Polylang strings, WooCommerce attribute lookups and Elementor cache flags all live in there, and at least one of them will be load-bearing for a subsite you forgot existed.

The usual culprits on Dutch SME stores: WooCommerce session rows that were never transient'd, Yoast indexable build artefacts, leftover WPML translation logs from a migration nobody finished, and the deeply optimistic debug payloads some plugin authors write straight into wp_options.

REST exposure on the fourteen plugins we always find

Every Dutch SME multisite we audit has the same plugin shortlist. The cast across roughly sixty audits is WooCommerce, Yoast SEO, Elementor, Advanced Custom Fields, WP Rocket, Wordfence, UpdraftPlus, Contact Form 7, WPForms, WP Mail SMTP, Polylang, MonsterInsights, Akismet, and Rank Math (where Yoast was migrated off). Each of these exposes its own REST namespace or rides on /wp/v2/. Some of those routes are fine for a public chat agent to read from. Some leak data the marketing lead would not want quoted back at them.

Enumeration is one curl per subsite:

curl -s https://shop2.example.nl/wp-json/ \
  | jq '.routes | keys[]' \
  | sort -u

Then we read each namespace against the WordPress REST API handbook and the plugin's own route map. The ones we score red on first pass:

  • GET /wp/v2/users returning anything other than an empty array for anonymous callers. WordPress changed the default behaviour in 4.7.1, but a long tail of older configs still leak author slugs.
  • GET /wc/store/cart exposed to the agent without nonce handling. The WooCommerce Store API is built for headless checkout and will return cart contents to whoever asks correctly.
  • GET /acf/v3/options/options when ACF is left at defaults. We have seen API keys sitting in the response.
  • Contact Form 7's /contact-form-7/v1/contact-forms listing form IDs and field names to anonymous callers, which is what gets scraped into form-spam targeting lists.

We do not block the agent from touching REST. We score which routes it can call as an unauthenticated client, which need an application password, and which it should never touch. That table goes into the quote.

The wp-cron question, and the 02:30 abandoned-cart job

WordPress's pseudo-cron is the part of the stack that breaks loudest when a chat agent goes live. The default wp-cron.php fires on page load, and a chat agent that increases traffic by even 30% will increase cron firing, which in turn fights the agent for PHP-FPM workers. The standard advice, per the WordPress developer handbook, is to set DISABLE_WP_CRON and hit wp-cron.php from a real system cron.

The question we have to answer per subsite before flipping that switch: what schedules currently depend on pseudo-cron firing whenever a visitor shows up? In Dutch e-commerce networks the answer almost always includes a WooCommerce abandoned-cart job scheduled around 02:30, because that is when the store is dead and the plugin author assumed traffic was zero. If wp-cron is disabled and no system cron replaces it, the 02:30 job silently stops running, and three weeks later somebody asks why recovery emails dried up.

Listing scheduled events with WP-CLI is fast:

for site in $(wp site list --field=url); do
  echo "== $site =="
  wp cron event list \
    --url="$site" \
    --fields=hook,next_run_relative,recurrence \
    --format=csv \
  | grep -Ei 'abandoned|cart|recovery|woocommerce'
done

What we report back is the answer to a precise question: which subsites would survive a DISABLE_WP_CRON flip without a real cron set up? On a typical fifteen-subsite Dutch network we usually find three. The three that survive are the corporate brochure subsite with no WooCommerce, the Dutch-language press-room subsite that only uses scheduled posts (which fire on visit anyway), and the staging-style subsite that an intern forgot to delete in 2022. Everything else needs a real system cron entry before the agent goes live.

The one-page hand-back

The output of the audit is a single A4 sheet per subsite with three scores: autoload weight in MB, REST exposure rated A through E against the plugin shortlist, and cron survival as yes or no. No prose. No executive summary. The marketing lead forwards it to their developer, and the developer either fixes the reds in a sprint or signs off on the agent retrofit waiting until they do.

The pattern under all of this is that the model is rarely the hard part. The hard part is the surface area the model has to talk to. A 6MB autoload table is surface area. A leaky /wp/v2/users is surface area. An orphaned 02:30 cron job is surface area. You can spend the agent budget making the model smarter, or you can spend it making the host stop fighting itself. We pick the second one every time.

The smallest thing you can do today, if you run a WordPress multisite and someone has floated a chat-agent project: open a terminal, run the autoload query above against every wp_N_options table, and write the answer in a single column on a piece of paper. If any number is above 4, the answer to "can we add a chat agent" is "not yet."

When we built the chat agent for a Rotterdam wholesaler running a nine-subsite WooCommerce network last winter, the autoload on their flagship subsite was 7.1MB and one of their REST routes returned every customer's email. We spent the first week of that project not on the agent at all. We spent it on the table.

Key takeaway

If wp_options autoload is over 4MB on any subsite, score the chat-agent retrofit as not yet. Fix the table first, then talk about the model.

FAQ

Why score wp_options autoload before anything else?

Every REST request loads autoloaded options into memory. A chat agent fires three to five REST calls per turn, so bloat compounds fast and turns a 200ms response into a one-second wait the user blames on the model.

Is 4MB a hard threshold or a guideline?

Guideline. Below 1MB is healthy; above 4MB is where PHP serialize and unserialize start costing more than the LLM round trip. Exact line depends on hosting, but 4MB is where we stop quoting new work.

Why list scheduled events per subsite before disabling wp-cron?

WordPress pseudo-cron fires on visitor load. Disabling it without a real system cron silently kills jobs like the 02:30 WooCommerce abandoned-cart sweep. The audit reports which subsites would notice and which would not.

What if the client refuses the pre-retrofit cleanup work?

We quote the agent at a higher rate, write the autoload number into the contract, and we both sign that we expect a slower widget. We have had exactly one client take that deal in two years.

wordpresschat agentsai agentsmysqlsecurityoperations

Building something?

Start a project