Measuring TTFB When a CDN Is Answering for You

Share
Measuring TTFB When a CDN Is Answering for You. Abstract performance illustration in orange and dark grey on debugly.dev

The team celebrated a TTFB improvement that nobody had shipped. The chart dropped the week the CDN was enabled, because from then on, most measurements were answered by an edge cache that never touched the application. The origin, meanwhile, was as slow as ever, and every user whose request missed the cache felt it.

TTFB behind a CDN is not one number. It is a mixture of the edge's answer time and the origin's answer time, weighted by the cache hit rate, and any measurement that does not separate the two is measuring the mixture.

This was a CDN in front of a Node 22.14 origin, measured with curl and real user metrics, Chrome 133.

Why the mixture lies

A cached response has a TTFB dominated by the edge's proximity and its own processing, typically tens of milliseconds. An origin response has a TTFB dominated by your application, which may be hundreds. The observed TTFB distribution is both, and the mean is a number that describes neither.

The mean TTFB improves as hit rate improves, even if the origin gets slower. So a dashboard can show TTFB trending down while the actual application, the thing you can change, trends up. This is the wrong denominator problem from Brotli saved forty percent on text, applied to time instead of bytes.

Measuring the origin on purpose

To see the origin, you must bypass the cache for the measurement, and there are a few honest ways.

Cache bypass header. Most CDNs honour a pragma for testing. Sending it returns the origin's response without a cache hit, letting you time the true origin path:

curl -s -o /dev/null -w '%{time_starttransfer}\n' \
  -H 'Cache-Control: no-cache' -H 'Pragma: no-cache' \
  https://example.com/page

The exact header depends on the CDN, and some require it to be enabled in configuration, so confirm yours accepts it rather than assuming.

A cache busting parameter. Appending a unique query string forces a miss for content that caches per URL:

curl -s -o /dev/null -w '%{time_starttransfer}\n' \
  "https://example.com/page?cb=$(date +%s%N)"

This only works for URLs whose cache key includes the query string, and it pollutes the cache with unique entries, so use it for measurement, not continuously.

Measure from behind the CDN. Curl directly to the origin host, bypassing the CDN entirely, if the origin is reachable. This gives the purest origin number, though it may differ from the CDN to origin path in TLS and routing.

Measuring the edge on purpose

Equally, to see what cached users get, force a hit. Request the same URL twice and use the second, or use a URL you know is hot. The edge number is its own truth, and it is the number most of your users experience for cacheable content.

The point is not that one number is right. It is that each number answers a different question, and the defect is recording them under one label.

Splitting the mixture in real user metrics

Synthetic curls sample one geography. Real user metrics give the distribution, and the distribution shows the mixture directly. Plot TTFB as a histogram and you will see two humps, a fast one for edge answers and a slow one for origin answers, with the hit rate deciding their relative size.

Once you see the bimodal shape, the mean is obviously meaningless, and the two modes become the two numbers to track. The fast mode tracks edge health. The slow mode tracks your application. Alert on the slow mode, because it is the one you own and the one that grows when the app regresses.

This is the tail argument from p99 and why averages lie, applied to a distribution that is literally two populations.

The hit rate is the meta metric

The single number that ties the two together is the cache hit rate. It decides how many users experience the edge versus the origin, so a drop in hit rate is a latency incident even if neither the edge nor the origin changed. Deployments that change cache keys or headers silently reduce hit rate and shift users onto the slow origin, which reads as "the site got slower" with no code having gotten slower.

Track hit rate next to the two TTFB modes. When the slow mode's share grows, look at what changed in caching behaviour, not in application code.

The rule

Behind a CDN, TTFB is a mixture of two systems with different owners and different remedies. Measure each on purpose, bypass the cache to time the origin, force a hit to time the edge, and in real user data treat the distribution's two modes as the two truths. Track hit rate as the valve between them.

Any single TTFB number recorded behind a CDN is a weighted average of the edge and the origin, and acting on it is steering by a number that describes a mixture you did not choose. The same mixture blindness appears whenever a fast path answers for a slow one, which is also the lesson of predictive search returning nothing at the correctness level.

The deploy that moves the mixture

There is one operational event that changes the mixture more than any code change, and it is worth naming because it is routine: a deploy that purges or invalidates the cache. For the window after the purge, the hit rate collapses and nearly every request walks the origin path, so TTFB jumps to the slow mode even though nothing about the application changed. Teams have chased that jump as a regression many times.

The discipline is to treat cache hit rate as part of the deploy's expected behaviour. Watch it recover after a purge, and read TTFB during the recovery window as origin performance, which is actually the purest origin sample you will ever get, delivered by accident. The purge window is the measurement you usually pay consultants to synthesise, and it happens for free on every invalidation.

Read more