Server takes too long to send the first byte
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
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.
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.
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.
Remove redirect hops. Every redirect before the final page is a full round trip added to TTFB.
http://tohttps://towww.to the page is three, and two of them are avoidable by linking to the final address.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.
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.
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 -> 200Passes -- TTFB 620 ms
https://www.example.com/pricing -> 200The 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
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).
Asks the PageSpeed Insights API about each of them, on mobile.
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.
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.
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.
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.
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.