# Reducing WordPress TTFB in 2026: The Number in Site Health Is Not Time to First Byte

> WordPress core calls a server response good under 600ms, but it times a request the server makes to itself and waits for the whole page. Which number to reduce first.

- Published: 2026-09-27
- Updated: 2026-10-04
- Author: xSpeed Cache Team
- Tags: TTFB, Performance, Tutorial, Hosting, WordPress, check:D1, platform:wordpress
- Canonical: https://xspeedcache.com/blog/reduce-ttfb-wordpress/

---

Updated September 2026

WordPress core calls a server response good below 600 milliseconds, a threshold read from the shipped 7.1.2 source on 27 September 2026. The [web.dev guidance](https://web.dev/articles/ttfb) that core's own docblock cites puts a good TTFB at 0.8 seconds or less. Both describe the same metric and neither measures the same quantity: core times a request the server makes to itself and waits for the entire page to arrive, while field data times real visitors from wherever they happen to be. Part of that gap you can change from inside WordPress. Part of it is geography. Work out which part is large before changing anything: the order of every fix below follows from that split.

## Quick summary: which number are you reducing?

| If the split shows | Do this first | Why |
|---|---|---|
| Think time high, network low | Cache the page | PHP never starts on a hit |
| Think time high with the cache hitting | The requests a cache cannot serve | Logged in, cart and REST all run PHP |
| Network high, think time low | Move the origin, or add an edge | No PHP tuning shortens a round trip |
| Both low, page still slow | Stop working on TTFB | The wait has moved to render |

![TTFB split into network time and think time](https://xspeedcache.com/images/blog/ttfb-network-vs-think-time.webp)

Three sources publish three different thresholds for this metric, and they disagree because each measures something different.

| Source | Threshold | What it actually measures |
|---|---|---|
| WordPress Site Health | 600 ms | Median of three loopback requests, first byte plus the body |
| web.dev field guidance | 0.8 s good, above 1.8 s poor | Real visitors at the 75th percentile |
| An external probe | Varies with distance | One city, network and think time together |

## The number in Site Health is not time to first byte

Reading `WP_Site_Health` in the shipped 7.1.2 source on 27 September 2026, [`check_for_page_caching()`](https://developer.wordpress.org/reference/classes/wp_site_health/check_for_page_caching/) loops three times. Each pass records `microtime( true )`, calls `wp_remote_get( home_url( '/' ) )`, then records it again. [`get_page_cache_detail()`](https://developer.wordpress.org/reference/classes/wp_site_health/get_page_cache_detail/) takes the median of those three and compares it against a threshold that the [`site_status_good_response_time_threshold`](https://developer.wordpress.org/reference/hooks/site_status_good_response_time_threshold/) filter defaults to 600.

Two things follow, neither of them visible in the screen that reports the result.

**It waits for the whole page, not the first byte.** [`wp_remote_get()`](https://developer.wordpress.org/reference/functions/wp_remote_get/) defaults to `'blocking' => true` and `'stream' => false`, so it returns only once the entire response body has been received. The interval core labels "Median server response time" therefore covers the first byte **and** the download of every kilobyte of your homepage HTML. Halve your homepage and that number falls without the server having got faster. Transfer weight is its own problem, and [the 350 KB cliff](https://xspeedcache.com/blog/page-weight-vs-lcp-wordpress/) covers where it starts to bite.

**It is the server asking itself.** The request goes to `home_url()` from inside PHP, which is why core's own error text asks you to verify that loopback requests are working. The network legs it crosses are the server's own, from inside the data centre, so a visitor's DNS lookup, TCP connect and TLS handshake are never part of it. The default timeout is 5 seconds, so a slower site reports that the response time could not be determined rather than a large number.

None of this makes the check wrong. It answers a narrower question than its label suggests.

## Split the half you can change from the half you cannot

`curl` reports each phase of a request separately, and its [manual page](https://curl.se/docs/manpage.html) defines `time_starttransfer` as including `time_pretransfer` "and also the time the server needed to calculate the result". That sentence is the whole method: subtract one field from the other and what remains is think time.

```bash
curl -o /dev/null -s -w \
"pre %{time_pretransfer}\nbyte1 %{time_starttransfer}\n" \
https://example.com/
```

Everything up to `time_pretransfer` is name resolution, the connect, the handshake and sending the request. Everything between the two fields happened on your server. Subtracting `time_pretransfer` rather than the TLS handshake matters: the handshake finishes before the request is sent, and the send is not think time. Run it three times so one cold connection is not your baseline.

When the network half is the large one, the cause is usually distance or a changed route rather than anything in WordPress, and [the host-migration fix guide](https://xspeedcache.com/blog/site-slower-after-host-change/) walks the phase-by-phase diagnosis for that case.

## Fix in the order the split dictates

**1. Serve the page from a cache.** The mechanism is worth stating plainly, because most guides skip it: a page cache does not make PHP faster. It stops PHP from running. On a hit the web server returns a stored file and WordPress never loads, regardless of how heavy the page was to build.

That only removes think time that exists. On our own scan archive, [nearly half of WordPress sites have under 100 milliseconds of headroom](https://xspeedcache.com/blog/when-not-to-use-caching-plugin/) for a page cache to take, so a site already answering in 90 milliseconds has little to win here.

**2. Then the requests the cache does not serve.** Everything below this line only ever touches a miss, so doing it before step 1 is work spent on requests that were about to stop happening.

## The requests a page cache never serves

A page cache stores anonymous HTML. Logged-in views, the cart, the checkout, admin-ajax and REST responses all bypass it by design, and on a busy site they are a large share of real traffic. [Signing in leaves the page cache behind entirely](https://xspeedcache.com/blog/slow-wordpress-logged-in/) covers why, and [WooCommerce stores](https://xspeedcache.com/use-cases/woocommerce/) feel it hardest because two of their most important pages can never be cached.

For those requests, two levers matter and one of them is almost never discussed.

**An object cache, on core's own terms.** A page-cache hit answers in 5 to 15 milliseconds because nothing is recomputed; an object cache changes what happens when there is no hit. Core does not leave the question of whether you need one to taste. The [`site_status_persistent_object_cache_thresholds`](https://developer.wordpress.org/reference/hooks/site_status_persistent_object_cache_thresholds/) filter defaults to `alloptions_count` 500, `alloptions_bytes` 100000, and 1,000 rows each for comments, options, posts, terms and users. Any single one of those tripping is enough for core to start suggesting a persistent object cache. Core does not warn about the *size* of autoloaded options until 800,000 bytes, so there is an eightfold band in which the fix is suggested and the problem is not yet called one. We build xSpeed Cache at WPDeveloper, and its [object cache documentation](https://xspeedcache.com/docs/object-cache/) covers Redis and Memcached setup.

**PHP worker exhaustion, which reads as a slow server and is not one.** [The PHP manual](https://www.php.net/manual/en/install.fpm.configuration.php) states that `pm.max_children` "sets the limit on the number of simultaneous requests that will be served", and that `listen.backlog` defaults to 511 on Linux. Requests arriving past the worker count are therefore accepted and then queued: the TCP connect completes normally, the TLS handshake completes normally, and think time balloons while nothing in your code has slowed down at all.

| What you see | Worker exhaustion | Genuinely slow code |
|---|---|---|
| Think time at low traffic | Normal | Already slow |
| Think time at peak | Several times worse | Roughly the same |
| `max children reached` counter | Climbing | Zero |

FPM's status page publishes the evidence directly: `listen queue`, `max listen queue`, `active processes` and `max children reached`. A `max children reached` count above zero says the number to reduce is concurrency, not code, and raising the worker count or removing what occupies workers is the fix. Our [server compatibility documentation](https://xspeedcache.com/docs/server-compatibility/) lists what the plugin needs from a host.

## Confirming the number actually moved

Start from outside the server, where your readers are. The free xSpeed Scan needs no account, and its check D1 reports time to first byte measured from outside your origin with the probe's own city named beside it, so the network half is visible rather than assumed.

A scan of wordpress.org on 27 September 2026 shows why one number is not enough. Check D1 recorded a TTFB of 540 milliseconds from Vilnius, while the Lighthouse run in the same report measured the server responding in 113 milliseconds from Google's own network. Same page, same minute, and roughly 427 milliseconds of the difference is distance rather than server work.

![540ms from Vilnius against 113ms of server time](https://xspeedcache.com/images/blog/ttfb-two-numbers-one-request.webp)

Run the free scan on your own site: [xspeedcache.com/scan/](https://xspeedcache.com/scan/)

If you would rather check by hand, run the `curl` split above three times before the change and three times after, and compare only the think-time figure. The total will move with your network conditions; the subtraction will not.

## Doing this across twenty sites instead of one

Checking one site by hand is quick. Checking twenty after a migration is a morning, and the per-site numbers tell you whether a shared origin is the cause. [xSpeed Hub](https://xspeedcache.com/xspeed-hub/) reads cache state and scan results across a fleet from one connection, which is the case it exists for. When the split says the origin itself is the limit, our standing recommendation is xCloud, and it is ours: xCloud and WPDeveloper are both Startise companies.

## Common mistakes when reducing TTFB

- **Changing PHP settings before reading the split.** If the network half is the large one, a faster PHP version moves nothing a visitor can feel.
- **Measuring once.** A cold connection and a cache miss both look like a slow server exactly once.
- **Chasing TTFB once it is already low.** Below 200 milliseconds the remaining wait is usually render.

## Frequently Asked Questions

### My Site Health says Good and visitors still wait. What is happening?

Core measured a loopback, and your visitors are not on your network. Run an external probe and compare; a large gap is distance.

### I enabled a page cache and the Site Health number barely moved. Why?

That check waits for the whole body. If the page is heavy the download dominates, and a cache hit cannot shorten it.

### What counts as a good TTFB for WordPress?

web.dev puts good at 0.8 seconds or less and poor above 1.8 seconds, on real visitors. Our own check D1 grades against 200 milliseconds.

### My cart page is slow and my homepage is fast. Is that normal?

Yes. The cart cannot be cached, so every request runs the whole of WordPress. Work on the object cache and worker count instead.

### Does a CDN reduce TTFB?

For distant visitors, substantially, because it shortens the round trips. For a visitor next door to your origin, very little.

### I cannot find pm.max_children anywhere on my host. What now?

Managed hosts expose it as a plan limit rather than a file. Ask support for your worker count and whether the FPM status page can be turned on.

### Does an object cache help a page that is already served from cache?

No. A hit never reaches PHP, so it queries nothing. An object cache pays off on misses and logged-in traffic.

### My TTFB got worse after a plugin update. How do I find which one?

Compare think time only, then deactivate in halves rather than one at a time. Five passes narrow twenty plugins to one.

### Is TTFB a Core Web Vital?

No. web.dev notes that TTFB is not a Core Web Vital, which is why a good TTFB and a failing LCP sit together so often.

### Can I raise the 600 millisecond threshold instead?

The filter allows it. Changing the verdict changes nothing a visitor feels, so do it only with a measured reason.

## Conclusion: measure the split, then fix in that order

TTFB rewards one honest measurement more than any configuration. Split it, find the large half, fix that half. Keep expectations right: removing the server entirely still leaves [93% of slow WordPress sites slow](https://xspeedcache.com/blog/caching-did-not-fix-slow-wordpress/), and when the first byte is already quick [the remaining wait belongs to render](https://xspeedcache.com/blog/good-ttfb-slow-page/).

**What to do this week:**

1. Run the `curl` split three times and write down the think-time figure only.
2. Run a scan from outside and compare it against what Site Health told you.
3. Serve anonymous pages from a cache, then measure again before touching PHP.
4. Check the `max children reached` counter before concluding your code is slow.
5. Install xSpeed Cache free from [wordpress.org](https://wordpress.org/plugins/xspeed/) and turn page caching on. Pro is $29 a year with a 14-day money-back guarantee, and the [Free vs Pro](https://xspeedcache.com/free-vs-pro/) page lists exactly where the line falls.

If the split points at the origin rather than the code, [pricing](https://xspeedcache.com/pricing/) and the [feature list](https://xspeedcache.com/features/) are the next pages to read.
