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:
- CDN and Edge Caching: Serve from edge locations close to users
- Database Optimization: Indexes, connection pooling, caching
- Parallel Processing: Don't wait for sequential operations
- Server Configuration: HTTP/2, compression, keep-alive
- 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.
