BlogTTFB: When Your Server, Not Your Frontend, Is the Slow PartScan · Site Quality

TTFB: When Your Server, Not Your Frontend, Is the Slow Part

OH
Omar Haddad · November 2025 · 10 min read

TL;DR

You compressed the images, deferred the JavaScript, shipped a CDN, and LCP is still bad. The culprit is upstream of everything you optimized: Time to First Byte, the time before your server even starts sending the page. Google's "good" TTFB is 800ms at the 75th percentile, and TTFB is the first phase of LCP, it is literally part of the formula. A server that takes 800ms to respond leaves only ~1,700ms of the 2.5s LCP budget for everything else, which on mobile is nearly impossible. Sites with poor LCP spend an average of 2.27 seconds on TTFB alone. No amount of frontend tuning can recover time the server already spent.

The optimization ceiling you cannot see

There is a specific kind of performance frustration that every senior engineer eventually hits. You do everything right on the frontend: next-gen images, critical CSS inlined, scripts deferred, fonts preloaded, a global CDN in front of it all. You re-run Lighthouse expecting a green LCP, and it is still red. The waterfall shows a long flat bar at the very start, before a single byte of HTML arrives. That bar is TTFB, and it is the ceiling on everything else you did. Every frontend optimization fights for the time after the first byte; none of them can touch the time before it.

Here is the relationship that makes it unavoidable. Per the Core Web Vitals reference on TTFB, Largest Contentful Paint decomposes into four parts: TTFB + resource load delay + resource load duration + element render delay. TTFB is the first term. It is not a separate metric that correlates with LCP, it is a component of it. Every millisecond your server spends before sending the first byte is a millisecond added directly to LCP, and no frontend work can claw it back. This is why two sites with identical front ends can post wildly different LCPs: the difference is entirely in the server.

The budget math: the "good" LCP threshold is 2,500ms. If TTFB is 800ms, you have ~1,700ms left for the browser to download, decode, and render the largest element. On mobile that is brutal. If TTFB is 1,500ms, a good LCP is essentially out of reach no matter how perfect your frontend is. TTFB does not just contribute to LCP, past a point, it makes a good LCP arithmetically impossible.

What "good" looks like, and how far off most sites are

Google's target is a TTFB of 800ms or less for 75% of users. That budget covers the whole pre-response phase: DNS resolution, the TCP handshake, TLS negotiation, redirects, and, usually the biggest chunk, your server actually generating the response. Per analysis cited by DebugBear on initial server response time, server processing typically dominates that budget, which means the fix is usually server-side, not network-side. Teams often reach for a CDN as the reflexive answer and are surprised when TTFB barely moves, because the CDN did not touch the part that was actually slow.

The field reality is grim. Only around 44% of mobile pages achieve a "good" TTFB globally, and sites with poor LCP spend an average of 2.27 seconds on TTFB alone, nearly the entire LCP threshold consumed before rendering even begins. If you are chasing a stubborn LCP, the odds are very good that your server, not your frontend, is the slow part. And because TTFB happens on the network before your analytics JavaScript runs, it is the easiest metric to ignore, there is nothing in the browser console screaming about it.

Where the server time actually goes

"The server is slow" is a symptom, not a diagnosis. TTFB has a handful of usual culprits, and you have to separate them to fix the right one.

Slow application code and database queries

The most common cause is the request handler itself: an N+1 query pattern, an unindexed lookup, a synchronous call to a slow downstream API, or rendering a heavy template on every request. This is pure server processing time and it is where most TTFB lives. Profile the request path and you will usually find one or two queries eating the budget. The classic case: a homepage that runs forty database queries to assemble a "personalized" module nobody clicks.

No caching, or caching the wrong layer

If every request regenerates the page from scratch, re-running the database queries and re-rendering the template, your TTFB is at the mercy of your slowest query under load. Full-page caching, object caching, and a CDN that caches HTML (not just static assets) move the response from "computed per request" to "served from memory." The single biggest TTFB win on most dynamic sites is putting a cache in front of pages that do not actually need to be dynamic.

Geographic and connection overhead

DNS, TCP, and TLS happen before your server sees the request. A user far from your origin pays round-trip latency on each handshake, and on a high-latency mobile connection, those handshakes are expensive. A CDN with edge presence near the user collapses that overhead, but only if it is actually caching the HTML, not just proxying through to a slow origin.

Redirects in the path

A redirect before the real response is pure TTFB inflation: an entire extra round trip before the server even begins generating the page the user asked for. A two-hop redirect chain can add hundreds of milliseconds of TTFB on every single request, even when nothing else is wrong.

Diagnose before you optimize

The first job is to separate server time from network time, because the fixes are completely different. The raw measurement is available in the browser's Navigation Timing API and from a simple curl, and it tells you whether you have a connection problem or a processing problem.

# Break TTFB into its phases with curl
$ curl -w "@-" -o /dev/null -s https://example.com <<'EOF'
  DNS lookup:    %{time_namelookup}s
  TCP connect:   %{time_connect}s
  TLS handshake: %{time_appconnect}s
  TTFB:          %{time_starttransfer}s
  Total:         %{time_total}s
EOF

# If time_starttransfer is far larger than time_appconnect,
# the server is the slow part, not the network.

In the field, capture the same split from real users via the Navigation Timing API so you know the 75th-percentile experience, not just your own:

const [nav] = performance.getEntriesByType('navigation');
const ttfb = nav.responseStart - nav.requestStart;     // server time
const dns  = nav.domainLookupEnd - nav.domainLookupStart;
const tcp  = nav.connectEnd - nav.connectStart;
const tls  = nav.connectEnd - nav.secureConnectionStart;

navigator.sendBeacon('/rum/ttfb', JSON.stringify({
  ttfb: Math.round(ttfb), dns: Math.round(dns),
  tcp: Math.round(tcp), tls: Math.round(tls),
  url: location.pathname,
}));
// Aggregate at p75. If ttfb dominates and dns/tcp are small,
// fix the application and caching, not the CDN config.
Insight: the question is never "is TTFB high?", it is "which phase is high?" If DNS/TCP/TLS are small and responseStart is large, the problem is your code or your cache, and a CDN swap will not help. If the handshakes are large, the problem is geography and connection reuse. The fix follows the phase, and guessing the phase wrong is how teams spend a quarter on a CDN migration that moves TTFB by 20ms.

The fixes, by where the time is

If server processing dominates: cache and profile

Add full-page caching for cacheable routes so the server returns a stored response instead of regenerating it. Add object/query caching for the expensive lookups behind dynamic pages. Then profile the remaining dynamic requests and kill the N+1 queries and unindexed lookups. This is the highest-leverage work because it attacks the dominant phase. Even partial caching, caching the expensive, slow-changing fragments of an otherwise dynamic page, can cut TTFB dramatically.

// Cache the expensive, slow-changing part; keep the dynamic shell fresh
const KEY = `category:${id}:products`;
let products = await cache.get(KEY);
if (!products) {
  products = await db.query(EXPENSIVE_QUERY, [id]);   // the slow part
  await cache.set(KEY, products, {ttl: 300});          // 5 min
}
// Render fast: the slow query ran once for 300s of requests, not per request

If handshakes dominate: edge and reuse

Put a CDN with edge nodes near your users in front of the origin, and make sure it caches HTML for cacheable routes, not just CSS and images. Enable HTTP/2 or HTTP/3 to reuse connections, and use preconnect for critical third-party origins so the handshake overlaps with other work instead of blocking.

<!-- Warm critical connections so handshakes don't block the first byte -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<link rel="dns-prefetch" href="https://cdn.example.com">

If redirects are in the path: flatten them

A redirect before your real response is pure TTFB inflation, a whole extra round trip before the server even starts on the page the user wanted. Collapse redirect chains to a single hop, and combine HTTPS and host normalization into one rule so you never stack two redirects on every request.

Gate TTFB so it does not creep back

TTFB regresses quietly. A new feature adds an uncached API call to the homepage handler. A query that was fast at 10,000 rows is slow at 10 million. A caching layer gets accidentally bypassed by a new cookie that makes every response uncacheable. None of these show up in code review, and all of them push TTFB, and therefore LCP, past budget. Assert a TTFB budget in CI and in synthetic monitoring so a server-side regression fails the build or pages you before users feel it.

# Synthetic check: fail if server response blows the budget
const TTFB_BUDGET_MS = 800;

const samples = [];
for (let i = 0; i < 5; i++) samples.push(await measureTTFB(url));  // warm cache
const p75 = samples.sort((a, b)=>a-b)[Math.floor(samples.length * 0.75)];

if (p75 > TTFB_BUDGET_MS) {
  console.error(`TTFB p75 ${p75}ms exceeds ${TTFB_BUDGET_MS}ms budget`);
  process.exit(1);
}

A scan that runs against your key templates and reports the TTFB breakdown, server versus network, gives you the diagnosis and the gate in one pass. It also catches the insidious case where TTFB is fine for the cached homepage but terrible for the long tail of uncached, parameterized URLs that real users actually hit.

The bottom line

TTFB is the slow part you cannot see from the frontend, because it happens before the frontend exists. It is the first term in the LCP formula, so a slow server caps every other optimization, you can compress and defer and preload all you want, but you cannot recover time the server already spent. Google's bar is 800ms, most mobile pages miss it, and poor-LCP sites burn over two seconds on TTFB alone. Diagnose which phase is slow (usually server processing), fix the dominant phase (caching and query profiling first, edge caching and connection reuse next), flatten any redirects in the path, and gate a TTFB budget in CI. A scan that separates server time from client time tells you, definitively, whether your problem is the server or the page, so you stop optimizing the half that was never slow.

Find the Gaps Before They Cost You

Scan audits your site for the accessibility, performance, AEO, and security gaps that quietly drain revenue and invite lawsuits, in one pass.

Try Scan Free →
Omar Haddad Omar Haddad writes about AI quality engineering at alt.qa, built by TheWorkCompany.