WordPress TTFB: The Number Everyone Quotes And Almost Nobody Measures Properly.

We measured TTFB on three real sites and bypassed their caches. One got 9x slower. Here is what the number is made of and which part you can fix.

Aditya Sharma·6 min read

Short answer: wordpress ttfb is not a measure of your server. It is DNS, plus a TCP connection, plus a TLS handshake, plus however long the server took to think. Only the last part is yours. And if you measured it as an ordinary visitor, you may have measured a cache rather than the site at all.

I do this measurement often at POSIMYTH, usually when somebody sends me a number and asks whether it is bad.

The honest answer is almost always: I cannot tell yet.

What the number is actually made of

Here are three real sites measured from the same machine, minutes apart, with the time split into its parts.

Stacked bar chart showing TTFB broken into DNS, TCP connect, TLS handshake and server think time for three sites. woocommerce.com totals 74ms with 19ms server think. wordpress.org totals 770ms with 256ms server think. protuno.com totals 851ms with 665ms server think
Measured with curl from one location, three runs each, fastest run shown. The red

segment is server think time, and it is the only segment your hosting and your WordPress install control. Yes, the slowest one is ours.*

Look at woocommerce.com. Total 74ms, of which the server spent 19ms.

The other 55ms was getting a connection open. No amount of caching plugin will change that part, because it is the speed of light and some certificate maths.

Now look at ours. 851ms total, 665ms of it server think time.

That is a genuinely different problem from wordpress.org’s 770ms, where the server accounted for 256ms and the connection setup for the rest. Two similar totals. Two unrelated causes.

This is why a bare TTFB comparison between two sites is close to meaningless. A site in Frankfurt measured from Mumbai will lose to a worse-built site next door.

The mistake that makes the number a lie

Here is the part I care most about, because it is the one that produces confidently wrong client reports.

When you load a site normally, you may be talking to a cache. So you are measuring the cache.

Append a query string nobody has ever requested and the cache has to go to the origin:

Table comparing normal requests against cache-bypassed requests. woocommerce.com returns 77ms normally and 693ms with a cache-buster, a 9x difference. wordpress.org returns 787ms normally and 1023ms bypassed, a 1.3x difference
Same URL, same machine, seconds apart. woocommerce.com is nine times slower once you

stop letting it answer from cache. That 77ms was real for visitors and completely wrong as a description of the server.*

Nine times.

Both numbers are true. They answer different questions. The cached number tells you what a visitor experiences. The bypassed number tells you what happens when the cache misses, which is exactly what happens to the first visitor after every deploy, every purge, and every uncached page like a cart or a checkout.

If you only ever measure the first one, you will be surprised by the second one on a Friday.

How to measure WordPress TTFB properly, in one command

curl -s -o /dev/null \
  -w 'dns=%{time_namelookup}  tcp=%{time_connect}  tls=%{time_appconnect}  ttfb=%{time_starttransfer}\n' \
  https://example.com/

Read it as differences, not as four separate numbers:

  • DNS is time_namelookup
  • Connect is time_connect minus DNS
  • TLS is time_appconnect minus time_connect
  • Server think time is time_starttransfer minus time_appconnect

That last subtraction is the whole trick, and it is the number worth writing down.

Then run it again with ?cachebust=$RANDOM on the end and write down both.

Run it three times, take the fastest, and note where you measured from. A single sample from a café wifi is not a measurement.

Read the headers before you believe anything

Before diagnosing, find out whether a cache was involved at all.

Table of cache headers from four real sites. woocommerce.com returns x-cache HIT. wordpress.org returns x-nc HIT. protuno.com returns cache-control s-maxage of one year but cf-cache-status DYNAMIC. cms.protuno.com returns cf-cache-status DYNAMIC
Row three is our own marketing site. It sends a one-year caching instruction and

Cloudflare answers DYNAMIC, meaning it is not being cached at the edge at all. I did not go looking for that. It turned up while measuring for this post, which is rather the point of measuring.*

That gap between “what the site asks for” and “what the cache did” is where most performance confusion lives.

Reference table decoding cache headers by stack: cf-cache-status for Cloudflare, x-cache for Fastly Varnish and CloudFront, age for any shared cache, x-litespeed-cache, x-nc, and cache-control marked as a request rather than a result
The last row is the one that catches people. cache-control is written by your own

site and only states an intention. Every other header here was written by something that actually made a decision.*

So what do you do with a slow number

Work in this order, because it goes cheapest to most disruptive.

Confirm which number is slow. Cached and bypassed both slow is an origin problem. Only bypassed slow means your cache is working and your origin is weak, which is survivable but fragile.

Check the server think time specifically. If time_starttransfer minus time_appconnect is under about 200ms, your PHP is not the problem and you should stop optimising it.

If connection setup dominates, the fix is geography, not code. A CDN in front, or hosting nearer the audience.

Only then look at WordPress. Slow plugins, an unindexed query, a bloated autoload table, a theme calling an external API on every page load. A plugin that stopped being maintained is a common culprit here, because nobody has profiled it in years.

Most of the WordPress performance advice on the internet starts at step four. On a lot of the sites I look at, the answer was in step three, and somebody spent a weekend on step four.

The part that does not scale

None of this is hard. It is two commands and a subtraction.

The problem is that it is two commands per site, and the answer changes every time hosting changes, a CDN rule is edited, or a plugin starts calling something slow on every request.

You will do it thoroughly during onboarding, and then not again until a client complains. That is the same shape as the unbilled half hour that every other check falls into.

Protuno’s Performance agent, Dash, carries a server response time check that measures origin rather than edge, on a schedule, across a portfolio. Being straight with you as always: Dash is built and named but not live yet. The free audit does run a server response check today, from the domain alone, alongside the caching headers described above.

Until the rest ships, the curl line is genuinely enough. Put it in whatever runbook you keep, next to the check for scheduled jobs that quietly stopped.

And measure your own site first. I did, and it is the slowest one in the chart.

Comments