Skip to content
ExponaBLOG
TECHNICAL SEO FOR DEVELOPERS · DAY 083 / 100

Time to First Byte matters: patterns, pitfalls and fixes

Every slow millisecond delays everything else.

Expona Admin7 min read

Time to First Byte matters: patterns, pitfalls and fixes because it determines whether your user waits seconds or milliseconds before your page even begins to render. Every slow millisecond delays everything else downstream—layout, paint, interaction. We've watched production sites lose conversion rates because TTFB crept above 500ms, and we've recovered them by fixing the patterns we'll walk through today.

Understanding TTFB and why it compounds delay

Time to First Byte (TTFB) is the time from the browser's initial request until it receives the first byte of the response. It includes network latency, server processing, and any middleware or CDN overhead. A high TTFB ceiling means everything downstream—Largest Contentful Paint (LCP), Cumulative Layout Shift (CLS), interaction readiness—gets delayed proportionally.

Consider a typical SaaS dashboard. If your server takes 800ms to render the initial HTML, the browser can't even start parsing the DOM, fetching stylesheets, or executing critical JavaScript. By the time the user sees anything meaningful, 1.5+ seconds have elapsed. In e-commerce or ride-hailing, that's often the difference between conversion and cart abandonment.

We measure TTFB in field data, not synthetic tests, because real users hit real network conditions, real database load, and real geographic distribution. A lab test from your office on a fiber connection won't catch the 75th percentile user in regional traffic.

Cache rendered pages

The fastest TTFB is no server request at all. The next fastest is serving a pre-rendered or cached version of your page.

Full-page caching works well when your content is either static (marketing pages, blog posts, documentation) or can be invalidated predictably (product listings, user dashboards). We typically cache at multiple layers:

  • Application cache: in-process or distributed (Redis, Memcached) for frequently computed responses
  • CDN cache: edge nodes geographically distributed for faster delivery
  • Browser cache: HTTP headers that instruct the browser to reuse assets

For a Next.js or NestJS application, here's a pattern we use for cached routes:

html
// NestJS example: cache route at application and HTTP level
import { Controller, Get, HttpCode } from '@nestjs/common';

@Controller('products')
export class ProductsController {
  constructor(private cache: CacheService) {}

  @Get(':id')
  @HttpCode(200)
  async getProduct(@Param('id') id: string) {
    // Check cache first
    const cached = await this.cache.get(`product:${id}`);
    if (cached) return cached;

    // Fetch and cache for 5 minutes
    const product = await this.db.products.findOne(id);
    await this.cache.set(`product:${id}`, product, 300);
    return product;
  }
}

In the HTTP response headers, set Cache-Control to signal cacheability to CDNs and browsers:

html
Cache-Control: public, max-age=300, s-maxage=3600

The s-maxage directive tells CDNs (not browsers) to cache for 1 hour even if browsers cache for 5 minutes. This dramatically reduces TTFB for repeat traffic hitting the edge.

For dynamic content (user-specific dashboards, order pages), cache at the database layer or use incremental static regeneration (ISR) in Next.js to pre-compute and re-render at intervals without blocking user requests.

Optimize slow database queries

Most TTFB problems we've diagnosed root to the database. A single N+1 query or full table scan can balloon TTFB from 50ms to 500ms+.

The pattern is familiar: your API endpoint joins users, orders, items, and shipments in a single request, and without proper indexing or query optimization, the database takes 400ms to assemble the response. Meanwhile, 10 other concurrent requests are queued, and your server connection pool exhausts.

We follow this checklist:

  1. Profile queries: Enable slow query logs in PostgreSQL (log_min_duration_statement) or use database monitoring tools (AWS RDS Enhanced Monitoring, DataDog) to identify offenders
  2. Index strategically: Add indexes on WHERE clauses, JOINs, and ORDER BY columns. Monitor index bloat.
  3. Batch or cache: If you must join 5 tables, compute once, cache the result, and invalidate on write
  4. Paginate and limit: Never fetch 10,000 rows when you display 20. Use LIMIT and offset-based or cursor-based pagination.
  5. Use select projections: Fetch only the columns you need, not SELECT *

Here's a real example from a CRM platform. The initial query took 1.2 seconds:

html
-- SLOW: N+1 and full scan
SELECT u.id, u.email, u.created_at
FROM users u
WHERE u.status = 'active'
ORDER BY u.created_at DESC;
-- Then loop in app: SELECT * FROM orders WHERE user_id = ?

We replaced it with:

html
-- FAST: single query, indexed columns, limit, projection
SELECT u.id, u.email, u.created_at, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 'active'
  AND u.created_at > NOW() - INTERVAL '30 days'
INDEX idx_users_status_created (status, created_at)
GROUP BY u.id
ORDER BY u.created_at DESC
LIMIT 50;

TTFB dropped from 1.2s to 80ms. The keys: index on filter columns, LEFT JOIN with aggregation instead of loop, LIMIT to bound result set, and column projection to reduce data transfer.

Serve from regions near users

Network latency is non-negotiable. If your server is in us-east-1 and your user is in Singapore, light travel alone adds 150–200ms to TTFB. If the user is on 4G in a rural area, add another 200–400ms.

We deploy application servers to multiple AWS regions (or equivalent in GCP/Azure) and use a geolocation-aware routing strategy:

  • Route 53 (AWS) or Cloud Armor (GCP): geolocation routing to send users to the nearest region
  • CDN edge functions: Cloudflare Workers, Lambda@Edge, or Compute@Edge run logic at edge nodes without a round trip to origin
  • Global load balancer: distribute traffic by proximity and current region capacity

A trading platform we worked with served European traders from us-east-1. Base TTFB was 350ms. After deploying to eu-west-1 and routing EU traffic there, TTFB dropped to 80–120ms. For real-time trading UI, that 250ms swing improved perceived responsiveness and reduced missed orders.

For static or semi-static content, CDNs are essential. Content served from an edge node 20ms away rather than a distant origin is night and day. Ensure your cache headers are correct so CDNs actually cache aggressively.

Measure TTFB in field data

Lab testing (Lighthouse, WebPageTest) is a start, but field data is the truth. Real users, real networks, real geography reveal the TTFB distribution you actually experience.

Use the Navigation Timing API to collect TTFB from real traffic:

html
// Collect TTFB from field data
if (window.performance && window.performance.timing) {
  const timing = window.performance.timing;
  const ttfb = timing.responseStart - timing.navigationStart;
  const dns = timing.domainLookupEnd - timing.domainLookupStart;
  const tcp = timing.connectEnd - timing.connectStart;
  const ttw = timing.responseStart - timing.requestStart; // time to first byte

  // Send to analytics
  fetch('/api/metrics', {
    method: 'POST',
    body: JSON.stringify({
      ttfb,
      dns,
      tcp,
      ttw,
      url: window.location.pathname,
      userAgent: navigator.userAgent,
    }),
  });
}

Aggregate this data by geography, device type, and page route. Use Google Analytics 4 or a custom dashboard to track percentiles over time. We typically monitor:

  • Median (p50) TTFB
  • 75th percentile (p75) TTFB
  • 95th percentile (p95) TTFB—where slower users experience your site

Set targets aligned with Core Web Vitals: TTFB should be <200ms for p75 and <600ms for p95 in most regions. If you're consistently above that, drill into the root cause (database, CDN cache hit ratio, server CPU) and prioritize the bottleneck.

Common mistakes

  • Caching without invalidation: Set cache TTLs too high and users see stale data. Invalidate on every write (tag-based invalidation in Redis, webhook-triggered CDN purges) or use short TTLs and accept some database load.
  • Relying on synthetic TTFB: Your Lighthouse score looks good, but real TTFB in the 95th percentile is 800ms because you didn't test from slow networks or distant regions. Field data trumps lab data.
  • Ignoring connection overhead: Even a 50ms database query means nothing if the user's DNS lookup is 100ms and TCP handshake is another 100ms. Measure the full pipeline.
  • Scaling vertically instead of geographically: Adding more CPU to a single region doesn't help a user 6,000 miles away. Deploy regionally first, then scale within regions.
  • Not measuring the actual user experience: You optimized TTFB to 100ms but forgot that the page sends 10 render-blocking JavaScript files. TTFB is a necessary condition, not sufficient.

Key takeaways

  • TTFB is the ceiling for all downstream performance metrics. Every millisecond of TTFB delay propagates through LCP, CLS, and interaction readiness.
  • Cache rendered pages at the application, CDN, and browser layers. Use proper Cache-Control headers and invalidate strategically.
  • Database queries are often the culprit. Profile with slow query logs, add indexes on filter and join columns, batch requests, and limit result sets.
  • Serve from regions near users. Geolocation routing and edge computing reduce network latency from hundreds of milliseconds to tens.
  • Measure TTFB in field data across percentiles, geographies, and device types. Synthetic TTFB is a starting point; real-world distribution drives decisions.
  • Monitor and alert on TTFB targets (p75 <200ms, p95 <600ms). Make it part of your performance SLO.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles

Time to First Byte matters | Technical SEO | Expona