Back to Blog

Time to First Byte (TTFB): How to Diagnose and Fix Slow Server Response Times

TTFB is the first performance metric users experience, yet it's often overlooked. Learn what affects Time to First Byte, how to measure it accurately, and proven techniques to optimize server response time�from CDN configuration to backend tuning.

Scanly App

Published

14 min read

Reading time

Related articles: Also see how TTFB improvements cascade into better LCP scores, a complete performance optimisation strategy covering TTFB and beyond, and caching strategies that are the fastest path to TTFB improvement.

Time to First Byte (TTFB): How to Diagnose and Fix Slow Server Response Times

Your website looks fast. JavaScript is optimized, images are compressed, you're using a CDN. But users still complain about slow page loads. You check the metrics and see something alarming: Time to First Byte (TTFB) is 1.2 seconds.

Before users see a single pixel, before any JavaScript executes, before images start loading�the server took 1.2 seconds just to start responding.

TTFB is the most fundamental web performance metric because it measures the time before anything else can happen. A slow TTFB creates a performance floor that no amount of frontend optimization can overcome. It's like having a car with a fast engine but taking 5 minutes to turn the key�nothing else matters until you fix that first step.

This guide explains what TTFB really measures, what causes high TTFB, and proven techniques to optimize server response time across your entire stack�from DNS and SSL to application code and database queries.

What is Time to First Byte?

Time to First Byte (TTFB) is the time between the browser making an HTTP request and receiving the first byte of the response. It encompasses:

graph LR
    A[User Clicks Link] --> B[DNS Lookup]
    B --> C[TCP Connection]
    C --> D[TLS Handshake]
    D --> E[HTTP Request]
    E --> F[Server Processing]
    F --> G[First Byte Received]

    style B fill:#ffccbc
    style C fill:#ffccbc
    style D fill:#ffccbc
    style E fill:#c5e1a5
    style F fill:#fff9c4
    style G fill:#bbdefb

TTFB Components

Component What It Measures Typical Time Optimization Target
DNS Resolution Looking up IP address 20-120ms Use fast DNS, prefetch
TCP Connection Establishing connection 50-200ms Use HTTP/2, keep-alive
TLS Handshake SSL/TLS negotiation 50-150ms Modern TLS, session resumption
Server Processing Backend generates response 50-500ms+ PRIMARY TARGET
Network Latency Physical distance 10-300ms Use CDN

TTFB = DNS + Connection + TLS + Request Send + Server Processing + Response Start

The most variable component? Server processing time.

Measuring TTFB

Browser DevTools

// Chrome DevTools Network tab
// Look at "Waiting (TTFB)" in the timing breakdown
// Green = good (< 200ms)
// Orange = needs improvement (200-600ms)
// Red = poor (> 600ms)

Navigation Timing API

// measure-ttfb.js
function measureTTFB() {
  const navigationTiming = performance.getEntriesByType('navigation')[0];

  if (navigationTiming) {
    const ttfb = navigationTiming.responseStart - navigationTiming.requestStart;
    console.log(`TTFB: ${ttfb.toFixed(2)}ms`);

    // Detailed breakdown
    const dns = navigationTiming.domainLookupEnd - navigationTiming.domainLookupStart;
    const tcp = navigationTiming.connectEnd - navigationTiming.connectStart;
    const tls = navigationTiming.requestStart - navigationTiming.secureConnectionStart;
    const serverProcessing = navigationTiming.responseStart - navigationTiming.requestStart;

    console.log('Breakdown:');
    console.log(`  DNS: ${dns.toFixed(2)}ms`);
    console.log(`  TCP: ${tcp.toFixed(2)}ms`);
    console.log(`  TLS: ${tls.toFixed(2)}ms`);
    console.log(`  Server: ${serverProcessing.toFixed(2)}ms`);

    return ttfb;
  }

  return null;
}

// Run after page load
window.addEventListener('load', () => {
  setTimeout(measureTTFB, 0);
});

Server-Side Monitoring

// ttfb-middleware.ts
import express from 'express';

export function ttfbMiddleware() {
  return (req: express.Request, res: express.Response, next: express.NextFunction) => {
    const startTime = Date.now();

    // Capture when first byte is sent
    const originalWrite = res.write;
    const originalEnd = res.end;

    let firstByteSent = false;

    const recordTTFB = () => {
      if (!firstByteSent) {
        firstByteSent = true;
        const ttfb = Date.now() - startTime;

        // Log to metrics
        console.log(`${req.method} ${req.path} - TTFB: ${ttfb}ms`);

        // Send to monitoring
        metrics.histogram('http.ttfb', ttfb, {
          method: req.method,
          route: req.route?.path || 'unknown',
          status: res.statusCode,
        });
      }
    };

    res.write = function (...args: any[]) {
      recordTTFB();
      return originalWrite.apply(res, args);
    };

    res.end = function (...args: any[]) {
      recordTTFB();
      return originalEnd.apply(res, args);
    };

    next();
  };
}

// Usage
app.use(ttfbMiddleware());

Synthetic Monitoring

// ttfb-monitoring.ts
import axios from 'axios';

interface TTFBResult {
  url: string;
  ttfb: number;
  dns: number;
  tcp: number;
  tls: number;
  waiting: number;
  total: number;
}

async function measureTTFB(url: string): Promise<TTFBResult> {
  const start = Date.now();

  try {
    const response = await axios.get(url, {
      // Capture timing information
      onDownloadProgress: (progressEvent) => {
        // First byte received
      },
    });

    const timings = response.request?.timings || {};

    return {
      url,
      ttfb: timings.waiting || 0,
      dns: timings.lookup || 0,
      tcp: timings.connect || 0,
      tls: timings.secureConnect || 0,
      waiting: timings.waiting || 0,
      total: Date.now() - start,
    };
  } catch (error) {
    throw new Error(`Failed to measure TTFB for ${url}: ${error.message}`);
  }
}

// Monitor multiple endpoints
async function monitorTTFB() {
  const endpoints = ['https://example.com/', 'https://example.com/api/products', 'https://example.com/api/users'];

  const results = await Promise.all(endpoints.map((url) => measureTTFB(url)));

  results.forEach((result) => {
    console.log(`\n${result.url}`);
    console.log(`  DNS: ${result.dns}ms`);
    console.log(`  TCP: ${result.tcp}ms`);
    console.log(`  TLS: ${result.tls}ms`);
    console.log(`  TTFB: ${result.ttfb}ms`);
    console.log(`  Total: ${result.total}ms`);

    if (result.ttfb > 600) {
      console.warn(`??  High TTFB detected!`);
    }
  });
}

// Run every 5 minutes
setInterval(monitorTTFB, 5 * 60 * 1000);

What's a Good TTFB?

Google's Core Web Vitals don't directly include TTFB, but it affects other metrics:

TTFB Range Rating Impact Action Required
0-200ms ? Good Minimal impact on UX Maintain
200-600ms ?? Needs Improvement Noticeable delay Optimize
600ms+ ? Poor Significant UX impact Fix immediately
1000ms+ ?? Critical Severe UX degradation Critical priority

Mobile: Add 50-100ms due to higher network latency

Root Causes of High TTFB

1. Network and Infrastructure

// Diagnose network issues
async function diagnoseNetwork(url: string) {
  const { performance } = require('perf_hooks');
  const https = require('https');
  const dns = require('dns').promises;

  const urlObj = new URL(url);

  console.log(`\n?? Diagnosing: ${url}\n`);

  // DNS lookup time
  const dnsStart = performance.now();
  const addresses = await dns.resolve4(urlObj.hostname);
  const dnsTime = performance.now() - dnsStart;

  console.log(`DNS Lookup: ${dnsTime.toFixed(2)}ms`);
  console.log(`  Resolved to: ${addresses[0]}`);

  if (dnsTime > 100) {
    console.warn(`??  Slow DNS (> 100ms). Consider:`);
    console.warn(`   - Using faster DNS provider`);
    console.warn(`   - DNS prefetching`);
    console.warn(`   - Reducing DNS lookups`);
  }

  // TCP + TLS connection time
  return new Promise((resolve) => {
    const startTime = performance.now();

    const req = https.get(url, {}, (res) => {
      const connectTime = performance.now() - startTime;

      console.log(`\nConnection Time: ${connectTime.toFixed(2)}ms`);

      if (connectTime > 250) {
        console.warn(`??  Slow connection (> 250ms). Consider:`);
        console.warn(`   - Using CDN closer to users`);
        console.warn(`   - HTTP/2 or HTTP/3`);
        console.warn(`   - Connection keep-alive`);
      }

      res.on('data', () => {});
      res.on('end', resolve);
    });

    req.on('error', (error) => {
      console.error(`Error: ${error.message}`);
      resolve();
    });
  });
}

// Run diagnosis
diagnoseNetwork('https://example.com/api/slow-endpoint');

2. Server-Side Processing

Database Queries

// ? BAD: N+1 query problem
async function getPostsWithAuthors() {
  const posts = await db.query('SELECT * FROM posts LIMIT 10');

  // For each post, query author separately (11 queries total!)
  for (const post of posts) {
    post.author = await db.query('SELECT * FROM users WHERE id = $1', [post.author_id]);
  }

  return posts;
}

// ? GOOD: Single query with JOIN
async function getPostsWithAuthors() {
  const posts = await db.query(`
    SELECT 
      posts.*,
      users.name as author_name,
      users.email as author_email
    FROM posts
    LEFT JOIN users ON posts.author_id = users.id
    LIMIT 10
  `);

  return posts;
}

Slow API Calls

// ? BAD: Sequential API calls
async function getUserDashboard(userId: string) {
  const user = await fetchUser(userId); // 200ms
  const projects = await fetchProjects(userId); // 300ms
  const activity = await fetchActivity(userId); // 250ms
  // Total: 750ms

  return { user, projects, activity };
}

// ? GOOD: Parallel API calls
async function getUserDashboard(userId: string) {
  const [user, projects, activity] = await Promise.all([
    fetchUser(userId),
    fetchProjects(userId),
    fetchActivity(userId),
  ]);
  // Total: 300ms (longest call)

  return { user, projects, activity };
}

Synchronous Operations

// ? BAD: Blocking file operation
import fs from 'fs';

app.get('/api/data', (req, res) => {
  const data = fs.readFileSync('/large/file.json', 'utf-8');
  res.json(JSON.parse(data));
});

// ? GOOD: Async file operation
import fs from 'fs/promises';

app.get('/api/data', async (req, res) => {
  const data = await fs.readFile('/large/file.json', 'utf-8');
  res.json(JSON.parse(data));
});

TTFB Optimization Techniques

1. CDN and Edge Caching

// cloudflare-workers-edge.ts
// Deploy at edge for minimal TTFB

addEventListener('fetch', (event) => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request: Request): Promise<Response> {
  const cache = caches.default;
  const cacheKey = new Request(request.url, request);

  // Check edge cache first
  let response = await cache.match(cacheKey);

  if (response) {
    // Cache hit - TTFB ~20-50ms
    return response;
  }

  // Cache miss - fetch from origin
  response = await fetch(request);

  // Cache at edge for future requests
  if (response.ok) {
    const cacheResponse = response.clone();
    event.waitUntil(cache.put(cacheKey, cacheResponse));
  }

  return response;
}

Caching Strategy per Content Type:

Content Type Cache Location TTL TTFB Impact
Static Assets CDN + Browser 1 year -90%
API Responses (public) CDN 5-60 min -80%
HTML Pages CDN 1-5 min -70%
Personalized Data No CDN cache N/A 0%

2. Database Optimization

Add Proper Indexes

-- Slow query (table scan)
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123;
-- Seq Scan on orders (cost=0.00..1234.56 rows=100)
-- Execution Time: 456ms

-- Add index
CREATE INDEX idx_orders_user_id ON orders(user_id);

-- Fast query (index scan)
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123;
-- Index Scan using idx_orders_user_id (cost=0.29..12.45 rows=100)
-- Execution Time: 2.3ms

Connection Pooling

// ? BAD: New connection per request
import { Client } from 'pg';

app.get('/api/data', async (req, res) => {
  const client = new Client({
    /* config */
  });
  await client.connect(); // 50-100ms
  const result = await client.query('SELECT * FROM data');
  await client.end();
  res.json(result.rows);
});

// ? GOOD: Connection pool
import { Pool } from 'pg';

const pool = new Pool({
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

app.get('/api/data', async (req, res) => {
  const client = await pool.connect(); // < 1ms (reuses connection)
  try {
    const result = await client.query('SELECT * FROM data');
    res.json(result.rows);
  } finally {
    client.release();
  }
});

Query Result Caching

// ttfb-query-cache.ts
import Redis from 'ioredis';

const redis = new Redis();

async function getCachedQuery<T>(cacheKey: string, query: () => Promise<T>, ttl: number = 300): Promise<T> {
  // Check cache
  const cached = await redis.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }

  // Execute query
  const result = await query();

  // Cache result
  await redis.setex(cacheKey, ttl, JSON.stringify(result));

  return result;
}

// Usage
app.get('/api/popular-products', async (req, res) => {
  const products = await getCachedQuery(
    'products:popular',
    () => db.query('SELECT * FROM products ORDER BY sales DESC LIMIT 20'),
    600, // 10 minutes
  );

  res.json(products);
});

3. Server Configuration

HTTP/2 and HTTP/3

# nginx.conf
server {
    listen 443 ssl http2;
    listen 443 quic reuseport;  # HTTP/3

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    # HTTP/3 headers
    add_header Alt-Svc 'h3=":443"; ma=86400';

    # Enable early hints (HTTP 103)
    http2_push_preload on;
}

Connection Keep-Alive

# nginx.conf
http {
    keepalive_timeout 65;
    keepalive_requests 100;

    upstream backend {
        server 127.0.0.1:3000;
        keepalive 32;  # Maintain 32 connections to backend
    }
}

Compression

// compression.ts
import compression from 'compression';
import express from 'express';

const app = express();

app.use(
  compression({
    level: 6, // Balance between speed and compression
    threshold: 1024, // Only compress responses > 1KB
    filter: (req, res) => {
      if (req.headers['x-no-compression']) {
        return false;
      }
      return compression.filter(req, res);
    },
  }),
);

4. Application-Level Optimization

Lazy Data Loading

// ? BAD: Load everything upfront
app.get('/api/user/:id', async (req, res) => {
  const user = await getUser(req.params.id);
  const posts = await getUserPosts(req.params.id);
  const followers = await getUserFollowers(req.params.id);
  const analytics = await getUserAnalytics(req.params.id);
  // TTFB: 600ms

  res.json({ user, posts, followers, analytics });
});

// ? GOOD: Load critical data first
app.get('/api/user/:id', async (req, res) {
  // Only load essential data for initial render
  const user = await getUser(req.params.id);
  // TTFB: 50ms

  res.json({
    user,
    // Include URLs for lazy loading
    _links: {
      posts: `/api/user/${req.params.id}/posts`,
      followers: `/api/user/${req.params.id}/followers`,
      analytics: `/api/user/${req.params.id}/analytics`,
    },
  });
});

Server-Side Caching

// server-cache.ts
import NodeCache from 'node-cache';

const cache = new NodeCache({ stdTTL: 600, checkperiod: 120 });

function cacheMiddleware(duration: number) {
  return (req: express.Request, res: express.Response, next: express.NextFunction) => {
    const key = `cache:${req.originalUrl}`;
    const cachedResponse = cache.get(key);

    if (cachedResponse) {
      // Instant response from memory
      return res.json(cachedResponse);
    }

    // Override res.json to cache response
    const originalJson = res.json.bind(res);
    res.json = (body: any) => {
      cache.set(key, body, duration);
      return originalJson(body);
    };

    next();
  };
}

// Usage
app.get('/api/products', cacheMiddleware(300), async (req, res) => {
  const products = await db.query('SELECT * FROM products');
  res.json(products);
});

Precomputation

// precompute-expensive-data.ts
import cron from 'node-cron';

// Precompute expensive aggregations
async function precomputeDashboardStats() {
  console.log('Precomputing dashboard stats...');

  const stats = await db.query(`
    SELECT 
      COUNT(*) as total_users,
      COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '24 hours') as new_users_24h,
      AVG(session_duration) as avg_session_duration
    FROM users
  `);

  // Store precomputed stats in Redis
  await redis.setex('dashboard:stats', 300, JSON.stringify(stats));
}

// Run every 5 minutes
cron.schedule('*/5 * * * *', precomputeDashboardStats);

// API endpoint returns precomputed data instantly
app.get('/api/dashboard/stats', async (req, res) => {
  const stats = await redis.get('dashboard:stats');
  res.json(JSON.parse(stats));
  // TTFB: ~5ms instead of 500ms
});

5. DNS and TLS Optimization

DNS Prefetching

<!-- Resolve DNS early for external resources -->
<link rel="dns-prefetch" href="https://api.example.com" />
<link rel="dns-prefetch" href="https://cdn.example.com" />
<link rel="dns-prefetch" href="https://fonts.googleapis.com" />

TLS Session Resumption

# nginx.conf
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets on;

# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /path/to/chain.pem;

TTFB Optimization Checklist

Immediate Wins (< 1 day)

  • Enable CDN caching for static assets
  • Add database indexes for slow queries
  • Enable compression (gzip/brotli)
  • Use connection pooling
  • Enable HTTP/2

Short-Term (1-2 weeks)

  • Implement application-level caching (Redis)
  • Optimize database queries (eliminate N+1)
  • Enable keep-alive connections
  • Add edge caching for API responses
  • Parallelize backend calls

Long-Term (1+ months)

  • Migrate to HTTP/3
  • Implement edge computing
  • Pre-render/precompute expensive pages
  • Database read replicas
  • Microservices optimization

Real-World TTFB Optimization Case Study

The Problem

E-commerce site with TTFB of 1.2 seconds for product pages, losing 15% of users before page load completion.

The Investigation

// Added detailed TTFB breakdown logging
{
  route: '/product/:id',
  ttfb: 1243ms,
  breakdown: {
    databaseQueries: 856ms,    // 69% of TTFB
    imageProcessing: 234ms,    // 19%
    recommendations: 123ms,    // 10%
    templating: 30ms          // 2%
  }
}

The Fixes

1. Database Optimization (856ms ? 45ms)

  • Added indexes on products.id, product_images.product_id
  • Eliminated N+1 query loading images and reviews
  • Connection pooling

2. Move Image Processing Off Request Path (234ms ? 0ms)

  • Process images on upload, not on request
  • Store multiple sizes in CDN

3. Cache Recommendations (123ms ? 5ms)

  • Compute recommendations async
  • Cache in Redis with 10-minute TTL

The Results

Metric Before After Improvement
TTFB 1,243ms 87ms 93% faster
Bounce Rate 15.3% 6.2% 59% reduction
Conversions 2.1% 3.4% 62% increase
Server Costs $4,200/mo $1,800/mo 57% reduction

Monitoring TTFB in Production

// production-ttfb-monitoring.ts
import { Histogram } from 'prom-client';

const ttfbHistogram = new Histogram({
  name: 'http_ttfb_seconds',
  help: 'Time to first byte in seconds',
  labelNames: ['method', 'route', 'status'],
  buckets: [0.05, 0.1, 0.2, 0.5, 1, 2, 5],
});

app.use((req, res, next) => {
  const start = process.hrtime();

  const originalWrite = res.write;
  res.write = function (...args: any[]) {
    const [seconds, nanoseconds] = process.hrtime(start);
    const ttfb = seconds + nanoseconds / 1e9;

    ttfbHistogram.observe(
      {
        method: req.method,
        route: req.route?.path || 'unknown',
        status: res.statusCode,
      },
      ttfb,
    );

    // Alert if TTFB exceeds threshold
    if (ttfb > 1.0) {
      console.warn(`??  High TTFB: ${(ttfb * 1000).toFixed(0)}ms for ${req.method} ${req.path}`);
    }

    res.write = originalWrite;
    return originalWrite.apply(res, args);
  };

  next();
});

Conclusion

TTFB is the foundation of web performance. Before users can see or interact with anything, the server must respond. A slow TTFB creates a performance ceiling that no amount of frontend optimization can overcome.

The key strategies for optimizing TTFB:

  1. CDN and Edge Caching: Serve from edge locations close to users
  2. Database Optimization: Indexes, connection pooling, caching
  3. Parallel Processing: Don't wait for sequential operations
  4. Server Configuration: HTTP/2, compression, keep-alive
  5. Application Caching: Redis, in-memory, precomputation

Start by measuring TTFB across your most important pages and API endpoints. Identify the bottlenecks using detailed timing breakdowns. Apply targeted optimizations, and monitor the impact.

Remember: A 100ms improvement in TTFB can translate to significant improvements in conversion rates, user satisfaction, and infrastructure costs.

Ready to monitor and optimize TTFB across your entire application? Sign up for ScanlyApp and get comprehensive performance monitoring with TTFB tracking, bottleneck detection, and optimization recommendations.

Related Posts