5.51 seconds to 0.25: three changes, and the one plugin that was eating the site

The site took 5.5 seconds to answer. It now takes
0.25. Same host, same theme, same content. Here is the whole thing in
three pictures.

Every page before and after

Every page. Red is before, green is after. The average went
5.51s → 0.25s, about 22×.

What actually changed

The three changes

Only three settings moved:

  • Page cache was off. I had turned it off to debug something months ago
    and never turned it back on.
  • Turning it on changed nothing. Bar two. That is the interesting one —
    see below.
  • One plugin was costing 4.59 seconds. See below that.

Gotcha #1: the cache was on and still not caching

Page cache enabled, engine set to Disk: Enhanced, and pages still took 5.35 seconds.
The plugin was printing the reason in an HTML comment at the bottom of every page the
whole time:

Page Caching using Disk: Enhanced (SSL caching disabled)

The site is HTTPS only. Every single request was an SSL request, so
every single request skipped the cache. One checkbox — “cache SSL (https)
requests” — and the bar fell off a cliff.

Gotcha #2: find the plugin

Every page cost the same 5.4 seconds — the fat homepage and a nearly empty
contact page alike. Flat cost like that is not content, it is something running on every
boot. So: switch off one plugin, measure, switch it back on. Nine times.

Per-plugin bisect

Eight plugins were free. wp-hide-post was 4.59 seconds.

Before touching it I checked what it was actually hiding: 13 published posts,
13 reachable by paging the blog, 13 in the sitemap
. It was hiding nothing. I
deactivated it and checked again — the same 13, byte for byte. Nothing was exposed
and nothing vanished.

The part where I was wrong for an hour

Every attempt to save the cache settings came back 403 Forbidden, and I
decided it was the host’s firewall — the same one that blocks the REST API here. It
was not. When I finally read the response body instead of the status code, it said:

The link you followed has expired.

That is WordPress, not a firewall. The settings page carries three hidden
_wpnonce fields and the last one is empty. PHP keeps the last
value of a repeated key, so the blank one overwrote the real security token on every
submit. My own form-filler was handing WordPress an empty nonce and WordPress was
correctly refusing it.

I also managed to “prove” that no plugin mattered, because my timing script was getting
a 406 from the firewall in 0.14 seconds and I had wrapped it in a bare
except: pass. A rejection looks exactly like a blazingly fast page if you only
measure the clock.

Read the body. Not the status code. And never let a test swallow its own errors.


Numbers measured with curl from a machine twenty miles from the server, two runs per
page, cold and warm. Nothing modelled.