Skip to content
Issue docs

Server takes too long to send the first byte

Standardslow_ttfbIssue 138

What is this issue?

Time to First Byte (TTFB) measures how long it takes from asking for a page to the first byte of the response arriving. It covers the redirect, the DNS lookup, the connection, the TLS handshake and the time the server spends building the page -- everything that happens before the browser has anything to work with.

Google's published bands are:

TTFB Verdict
800 ms or less Good
800 ms to 1.8 s Needs improvement
more than 1.8 s Poor

This check reports pages in the poor band.

TTFB is not a Core Web Vital. It is a diagnostic: it is not scored by Google directly and it is not a ranking signal on its own. It is here because it is the first thing to check when Largest Contentful Paint is poor -- nothing can render before the HTML arrives, so a page with a 2-second TTFB has already spent half its LCP budget before the browser has seen a single tag.

For a page to pass, the server should begin responding within 800 ms.

Why it matters

  • It is the floor under every other timing. LCP, First Contentful Paint and the moment the page becomes interactive are all measured from the same start. Time spent before the first byte is time none of them can get back.
  • It affects every page, not one element. An image can be optimised for one template. A slow origin is slow for the whole site, on every visit.
  • It shapes how much a crawler will fetch. A site that is slow to answer is fetched more slowly, which matters on large sites where crawl coverage is already a constraint.
  • The fixes are usually infrastructure, not content. Caching, a CDN and a quicker database query change nothing about the page and often move it from poor to good in one step.

This is a standard finding rather than an important one, deliberately. TTFB is not itself a ranking input, and on most sites a page failing this check is already failing "Largest Contentful Paint in the poor range" for the same underlying cause -- so grading it alongside the Core Web Vitals would charge the site twice for one fault. It has its own check because the fix is a different one, in a different place, and usually an easier one.

How to fix it

  1. Cache the HTML. The single largest win on most sites. A page that can be served from a cache does not wait for an application, a template engine or a database at all.

  2. Serve from somewhere near the visitor. A CDN in front of the origin removes most of the round-trip time, and an edge cache removes the origin from the path entirely for anything that is not personalised.

  3. Find the slow query. If the page is rendered per request, TTFB is usually one unindexed query or an N+1 in a loop. Profile the request rather than guessing.

  4. Remove redirect hops. Every redirect before the final page is a full round trip added to TTFB. http:// to https:// to www. to the page is three, and two of them are avoidable by linking to the final address.

  5. Make sure the connection itself is quick. Keep TLS 1.3 and HTTP/2 or HTTP/3 on, and keep connections alive so the handshake is not repeated.

  6. Do the slow work somewhere else. Sending an email, calling a third-party API or writing an analytics record during the request adds its whole duration to TTFB. Move it to a background job.

  7. Check it under load, not on an idle server. TTFB that is fine at midnight and poor at midday is a capacity problem, and the fix is scaling rather than code.

Examples

1. Rendered from the database on every request

A category page ran the same eleven queries for every visitor, including one that counted all products without an index.

Fails -- TTFB 2.4 s

The page markup, the images and the JavaScript were all fine. Nothing on the page was the problem.

Passes -- TTFB 140 ms

The rendered HTML is cached for five minutes at the edge and revalidated in the background. The first visitor after a change waits; nobody else does.

LCP fell from 4.6 s to 2.0 s at the same time, with no change to the page itself.

2. Three redirects before the page

Links in the sitemap and in an email campaign pointed at the plain HTTP, non-www address, so every visit made three round trips before the real request.

Fails -- TTFB 1.9 s

http://example.com/pricing   -> 301
https://example.com/pricing  -> 301
https://www.example.com/pricing -> 200

Passes -- TTFB 620 ms

https://www.example.com/pricing -> 200

The redirects still exist for anyone who types the old address; they are simply not on the path any of your own links take.

3. A third-party call inside the request

The product page asked an inventory API for live stock before rendering, and that API was slow.

Fails -- TTFB 3.1 s

Passes -- TTFB 210 ms

The page renders immediately with the stock level fetched by the browser after paint, or read from a cache the inventory service updates.

How PixyScan detects this

  1. Chooses which pages to measure. After the crawl, PixyScan measures the homepage plus the pages your own site links to most, up to a per-scan budget (ten pages by default).

  2. Asks the PageSpeed Insights API about each of them, on mobile.

  3. Prefers real-user data. Where the Chrome UX Report has enough traffic for that exact URL, TTFB is the 75th percentile of what real Chrome users experienced -- the whole navigation, including redirects, DNS, connection and TLS, on their networks and at their distance from your server. That is the quantity Google's thresholds are written for.

  4. Treats the lab reading as a floor, not a verdict. Where the report has no data for the URL, the number shown is the server response time from the PageSpeed lab run -- which measures the document response alone and leaves out the redirects and the connection setup. It is therefore always LOWER than the real TTFB.

    A floor can prove one thing and not the other. If even the floor is past 1.8 seconds, the page is genuinely slow and this check is raised. If the floor is comfortably under 800 ms, that proves nothing -- a site with a 1.2-second redirect chain in front of a 700 ms server would look fast -- so no "Good" verdict is given to a lab reading. The figure is shown, and the report says it is a floor.

  5. Ignores origin-level data for a page verdict. When the report has too little data for the URL, PageSpeed answers with the whole site's distribution. That is a statement about the site, not this page, so it is not used here.

  6. Raises the issue only above 1.8 seconds -- Google's own "poor" bound. A page at exactly 1.8 seconds is "needs improvement" and raises nothing.

  7. Raises nothing on a page it did not measure. A page outside the sample, and a page whose measurement failed, are both reported as Not measured.

This is a different measurement from the response time shown on the page report's fetch line, which is what PixyScan's own crawler experienced from its own network. Both are useful; only this one is compared against Google's threshold.

What we store

Storage Level

Page Level


Database Table / Prisma Model

PagePerformance (page_performance), with the finding itself on audit_issues.details.


Fields Used

Field Type Description
strategy String mobile or desktop. Every number in the row belongs to one form factor.
fetchedAt DateTime When PageSpeed answered. "Measured three weeks ago" is not "not measured".
errorReason String? Why a sampled page still has no numbers. Null when the call succeeded.
performanceScore Int? Lighthouse's performance category, rescaled 0-100.
labTtfbMs Int? Server response time from the PageSpeed lab run, in milliseconds.
fieldTtfbMs Int? TTFB from the Chrome UX Report for this URL. Null when the report has no data for it.

On the finding (audit_issues.details): metric, source (field or lab), valueMs and thresholdMs (1800).


Detection Dependencies

  • PageSpeed Insights API v5 (lighthouseResult)
  • Chrome UX Report field data (loadingExperience), where the URL has the traffic
  • The crawl's internal link graph, which decides which pages are sampled

Note

A page with no row was never measured. The pass samples the homepage plus the most-linked pages within a per-scan budget (PERFORMANCE_SAMPLE_LIMIT, default 10), so most pages of a site have no page_performance row at all. That is the normal state, it is drawn as Not measured, and this check cannot be raised against it.

A row whose metric columns are all null with an errorReason set is a different state again: the page WAS sampled and the measurement failed. It is still never reported as passing.

Further reading