- Home
- HTTP Headers
- Server-Timing Header
Header
Server-Timing Header
Learn how the Server-Timing header communicates server-side performance metrics to browsers. Analyze backend timing, database queries, and optimize performance.
TL;DR: Provides detailed backend performance metrics visible in browser DevTools and accessible via JavaScript. Use to identify server-side bottlenecks and optimize performance.
What is Server-Timing?
The Server-Timing header allows servers to communicate timing metrics about the request-response cycle to the browser. These metrics appear in browser DevTools and can be accessed via JavaScript, helping developers understand where time is spent on the server.
Think of it as a performance breakdown that shows “database query took 50ms, cache lookup took 5ms, rendering took 30ms” - giving you insight into server-side bottlenecks.
How Server-Timing Works
Server sends timing metrics:
HTTP/1.1 200 OK
Content-Type: application/json
Server-Timing: cache;desc="Cache Read";dur=23.2, db;dur=53, app;dur=47.2
{"users": [...]}
```text
**Browser displays in DevTools:**
Metrics appear in Network tab → Timing section, showing:
- cache: 23.2ms (Cache Read)
- db: 53ms
- app: 47.2ms
## Syntax
```http
Server-Timing: <metric-name>;dur=<duration>;desc="<description>"
Server-Timing: <metric1>, <metric2>, <metric3>
Parameters
- metric-name - Identifier for the metric (required)
- dur - Duration in milliseconds (optional)
- desc - Human-readable description (optional)
Common Examples
Database Query Timing
Server-Timing: db;dur=53.5;desc="Database Query"
```text
Single database operation timing.
### Multiple Metrics
```http
Server-Timing: cache;dur=10.5;desc="Redis Cache",
db;dur=42.3;desc="PostgreSQL Query",
render;dur=15.8;desc="Template Render"
Complete request breakdown.
API Gateway Metrics
Server-Timing: auth;dur=5.2;desc="Authentication",
validate;dur=2.1;desc="Input Validation",
process;dur=78.4;desc="Business Logic",
serialize;dur=3.8;desc="JSON Serialization"
```text
### Microservices Timing
```http
Server-Timing: user-service;dur=45.2,
product-service;dur=32.1,
payment-service;dur=67.8,
total;dur=145.1
Timing for downstream service calls.
Real-World Scenarios
E-commerce Product Page
HTTP/1.1 200 OK
Server-Timing: cdn-cache;dur=0;desc="CDN Cache Hit",
db-product;dur=12.3;desc="Product Database",
db-reviews;dur=8.7;desc="Reviews Database",
recommendations;dur=45.2;desc="ML Recommendations",
render;dur=18.5;desc="SSR"
```text
### GraphQL API
```http
HTTP/1.1 200 OK
Server-Timing: parse;dur=1.2;desc="Query Parsing",
validate;dur=0.8;desc="Query Validation",
execute;dur=89.4;desc="Query Execution",
user-resolver;dur=23.1,
posts-resolver;dur=45.2,
comments-resolver;dur=21.1
Search API
HTTP/1.1 200 OK
Server-Timing: elasticsearch;dur=145.3;desc="Search Query",
cache-check;dur=2.1;desc="Cache Lookup",
ranking;dur=34.5;desc="Results Ranking",
personalization;dur=12.8;desc="Personalization"
```text
### Cached vs Uncached Response
```http
# Cache Hit
HTTP/1.1 200 OK
Server-Timing: cache;dur=2.3;desc="Redis Cache Hit"
# Cache Miss
HTTP/1.1 200 OK
Server-Timing: cache;dur=1.8;desc="Cache Miss",
db;dur=67.4;desc="Database Query",
cache-write;dur=3.2;desc="Cache Update"
Server Implementation
Node.js (Express)
const express = require('express')
const app = express()
// Middleware to track timing
app.use((req, res, next) => {
req.timings = {
start: Date.now(),
marks: {}
}
// Helper to add timing marks
req.timeMark = (name, description) => {
req.timings.marks[name] = {
duration: Date.now() - req.timings.start,
description
}
}
// Set Server-Timing header before sending
const originalSend = res.send
res.send = function (data) {
const timings = Object.entries(req.timings.marks)
.map(([name, { duration, description }]) => {
let metric = `${name};dur=${duration}`
if (description) metric += `;desc="${description}"`
return metric
})
.join(', ')
if (timings) {
res.set('Server-Timing', timings)
}
originalSend.call(this, data)
}
next()
})
// Use in routes
app.get('/api/users', async (req, res) => {
// Check cache
const cacheStart = Date.now()
const cached = await redis.get('users')
req.timeMark('cache', 'Redis Cache Lookup')
if (cached) {
return res.json(JSON.parse(cached))
}
// Query database
const dbStart = Date.now()
const users = await db.query('SELECT * FROM users')
req.timeMark('db', 'Database Query')
// Cache result
await redis.set('users', JSON.stringify(users), 'EX', 3600)
req.timeMark('cache-write', 'Cache Update')
res.json(users)
})
```javascript
### Dedicated Timing Class
```javascript
class ServerTiming {
constructor() {
this.metrics = []
}
startTimer(name) {
return {
name,
start: performance.now()
}
}
endTimer(timer, description) {
const duration = performance.now() - timer.start
this.metrics.push({
name: timer.name,
duration: duration.toFixed(2),
description
})
}
async measure(name, description, fn) {
const start = performance.now()
try {
return await fn()
} finally {
const duration = performance.now() - start
this.metrics.push({
name,
duration: duration.toFixed(2),
description
})
}
}
toString() {
return this.metrics
.map((m) => {
let str = `${m.name};dur=${m.duration}`
if (m.description) str += `;desc="${m.description}"`
return str
})
.join(', ')
}
}
// Usage
app.get('/api/products/:id', async (req, res) => {
const timing = new ServerTiming()
const product = await timing.measure('db', 'Product Query', async () => {
return await db.products.findById(req.params.id)
})
const reviews = await timing.measure('reviews', 'Reviews Query', async () => {
return await db.reviews.findByProductId(req.params.id)
})
const recommendations = await timing.measure('recommendations', 'ML Model', async () => {
return await getRecommendations(req.params.id)
})
res.set('Server-Timing', timing.toString())
res.json({ product, reviews, recommendations })
})
Python (Flask)
from flask import Flask, g, request
from time import time
app = Flask(__name__)
class ServerTiming:
def __init__(self):
self.metrics = []
def add_metric(self, name, duration, description=None):
metric = {'name': name, 'duration': duration}
if description:
metric['description'] = description
self.metrics.append(metric)
def __str__(self):
parts = []
for m in self.metrics:
s = f"{m['name']};dur={m['duration']:.2f}"
if 'description' in m:
s += f";desc=\"{m['description']}\""
parts.append(s)
return ', '.join(parts)
@app.before_request
def before_request():
g.timing = ServerTiming()
g.start_time = time()
@app.after_request
def after_request(response):
if hasattr(g, 'timing'):
response.headers['Server-Timing'] = str(g.timing)
return response
@app.route('/api/users')
def get_users():
# Cache lookup
cache_start = time()
cached = redis.get('users')
g.timing.add_metric('cache', (time() - cache_start) * 1000, 'Redis Lookup')
if cached:
return jsonify(json.loads(cached))
# Database query
db_start = time()
users = db.session.query(User).all()
g.timing.add_metric('db', (time() - db_start) * 1000, 'Database Query')
# Serialize
serialize_start = time()
result = [user.to_dict() for user in users]
g.timing.add_metric('serialize', (time() - serialize_start) * 1000, 'Serialization')
return jsonify(result)
```javascript
### Django Middleware
```python
import time
from django.utils.deprecation import MiddlewareMixin
class ServerTimingMiddleware(MiddlewareMixin):
def process_request(self, request):
request._timing_start = time.time()
request._timing_metrics = []
def add_metric(self, request, name, duration, description=None):
metric = f"{name};dur={duration:.2f}"
if description:
metric += f';desc="{description}"'
request._timing_metrics.append(metric)
def process_response(self, request, response):
if hasattr(request, '_timing_metrics') and request._timing_metrics:
response['Server-Timing'] = ', '.join(request._timing_metrics)
return response
# Usage in views
def product_view(request, product_id):
# Database query
db_start = time.time()
product = Product.objects.get(id=product_id)
db_duration = (time.time() - db_start) * 1000
# Add timing
middleware = ServerTimingMiddleware()
middleware.add_metric(request, 'db', db_duration, 'Product Query')
return JsonResponse({'product': product.to_dict()})
Go (net/http)
package main
import (
"fmt"
"net/http"
"time"
)
type ServerTiming struct {
metrics []string
}
func (st *ServerTiming) AddMetric(name string, duration time.Duration, description string) {
metric := fmt.Sprintf("%s;dur=%.2f", name, float64(duration.Microseconds())/1000)
if description != "" {
metric += fmt.Sprintf(`;desc="%s"`, description)
}
st.metrics = append(st.metrics, metric)
}
func (st *ServerTiming) String() string {
result := ""
for i, m := range st.metrics {
if i > 0 {
result += ", "
}
result += m
}
return result
}
func timingMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
timing := &ServerTiming{}
r = r.WithContext(context.WithValue(r.Context(), "timing", timing))
next(w, r)
w.Header().Set("Server-Timing", timing.String())
}
}
func productsHandler(w http.ResponseWriter, r *http.Request) {
timing := r.Context().Value("timing").(*ServerTiming)
// Database query
dbStart := time.Now()
products := queryDatabase()
timing.AddMetric("db", time.Since(dbStart), "Database Query")
// Cache update
cacheStart := time.Now()
updateCache(products)
timing.AddMetric("cache", time.Since(cacheStart), "Cache Update")
json.NewEncoder(w).Encode(products)
}
```javascript
## Client-Side Access
### JavaScript Performance API
```javascript
// Access Server-Timing via Performance API
const perfEntries = performance.getEntriesByType('navigation')
const serverTiming = perfEntries[0].serverTiming
serverTiming.forEach((entry) => {
console.log(`${entry.name}: ${entry.duration}ms - ${entry.description}`)
})
// For fetch requests
fetch('/api/users').then((response) => {
const entries = performance.getEntriesByType('resource')
const lastEntry = entries[entries.length - 1]
if (lastEntry.serverTiming) {
lastEntry.serverTiming.forEach((timing) => {
console.log(`${timing.name}: ${timing.duration}ms`)
})
}
})
React Hook for Monitoring
import { useEffect } from 'react'
function useServerTiming(url) {
useEffect(() => {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.serverTiming) {
console.log(`Server metrics for ${entry.name}:`)
entry.serverTiming.forEach((timing) => {
console.log(` ${timing.name}: ${timing.duration}ms`)
})
}
})
})
observer.observe({ entryTypes: ['navigation', 'resource'] })
return () => observer.disconnect()
}, [url])
}
// Usage
function ProductPage() {
useServerTiming('/api/products')
// Component code...
}
```text
## Best Practices
### 1. Use Meaningful Names
```http
# ✅ Clear, descriptive names
Server-Timing: db-users;dur=45.2;desc="User Table Query",
db-orders;dur=67.8;desc="Orders Table Query"
# ❌ Cryptic names
Server-Timing: t1;dur=45.2, t2;dur=67.8
2. Include Descriptions
# ✅ Helpful descriptions
Server-Timing: ml;dur=234.5;desc="Recommendation Model Inference"
# ⚠️ Missing context
Server-Timing: ml;dur=234.5
```text
### 3. Round to Reasonable Precision
```http
# ✅ 1-2 decimal places
Server-Timing: db;dur=45.23
# ❌ Excessive precision
Server-Timing: db;dur=45.23847392847
4. Don’t Expose Sensitive Information
# ❌ Reveals internal architecture
Server-Timing: aws-rds-primary;dur=45.2;desc="production-db-1.amazonaws.com"
# ✅ Generic description
Server-Timing: db;dur=45.2;desc="Database Query"
```javascript
### 5. Consider Performance Impact
```javascript
// ✅ Minimal overhead
const start = performance.now()
await doWork()
timing.add('work', performance.now() - start)
// ❌ Excessive timing calls
for (let item of items) {
const start = performance.now()
processItem(item) // Very fast operation
timing.add('item', performance.now() - start)
}
Common Patterns
Cache Hit/Miss Tracking
# Cache hit
Server-Timing: cache;dur=2.1;desc="Cache Hit"
# Cache miss
Server-Timing: cache;dur=1.5;desc="Cache Miss",
db;dur=89.3;desc="Database Query",
cache-write;dur=4.2;desc="Cache Update"
```text
### Nested Service Calls
```http
Server-Timing: auth-service;dur=12.3,
user-service;dur=45.6,
permission-service;dur=23.1,
total-upstream;dur=81.0,
processing;dur=34.2
Progressive Enhancement
Server-Timing: base-query;dur=45.2;desc="Core Data",
enrich-user;dur=12.3;desc="User Enrichment",
enrich-product;dur=23.1;desc="Product Enrichment",
enrich-analytics;dur=8.7;desc="Analytics Data"
```javascript
## Monitoring and Analytics
### Send to Analytics
```javascript
// Send server timing to analytics
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.serverTiming) {
entry.serverTiming.forEach((timing) => {
analytics.track('server-timing', {
metric: timing.name,
duration: timing.duration,
url: entry.name
})
})
}
})
})
observer.observe({ entryTypes: ['navigation', 'resource'] })
Alert on Slow Operations
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.serverTiming) {
entry.serverTiming.forEach((timing) => {
if (timing.duration > 1000) {
// Alert on operations > 1 second
console.error(`Slow server operation: ${timing.name} (${timing.duration}ms)`)
errorReporting.notify(`Slow operation: ${timing.name}`, {
duration: timing.duration,
url: entry.name
})
}
})
}
})
})
```text
## Security Considerations
### Don't Leak Sensitive Data
```http
# ❌ Exposes database structure
Server-Timing: query-users-table;dur=45.2;desc="SELECT * FROM prod_users WHERE ..."
# ✅ Generic description
Server-Timing: db;dur=45.2;desc="Database Query"
Production vs Development
// Only include detailed metrics in development
const serverTiming = new ServerTiming()
if (process.env.NODE_ENV === 'development') {
serverTiming.addMetric('db-query', duration, query.toString())
} else {
serverTiming.addMetric('db', duration, 'Database Query')
}
Browser Support
Server-Timing is well-supported:
- Chrome 65+
- Firefox 61+
- Safari 15+
- Edge 79+
Related Headers
- Timing-Allow-Origin - Control timing data access
- Performance-Timing - Browser performance metrics
- X-Response-Time - Total response time (non-standard)
Privacy Considerations for Server-Timing
Server-Timing metrics are visible to any JavaScript running on the page, including third-party scripts. Avoid including information that reveals internal architecture details, database table names, query strings, or user-specific data in metric names or descriptions. A metric named db;dur=45 is safe; a metric named query-users-by-email;dur=45;desc="SELECT * FROM users WHERE email=..." is not.
For cross-origin resources, Server-Timing data is only accessible to JavaScript if the resource also includes a Timing-Allow-Origin header that matches the page origin. Without it, the serverTiming array on the PerformanceResourceTiming entry will be empty, even though the header was sent. This provides a natural privacy boundary for authenticated API responses.
Frequently Asked Questions
What is Server-Timing?
Server-Timing provides detailed backend performance metrics. It can include multiple named metrics with durations and descriptions, accessible via Performance API.
How do I read Server-Timing in JavaScript?
Use Performance API: performance.getEntriesByType("resource").forEach(e => console.log(e.serverTiming)). Each entry has name, duration, and description.
What metrics should I include?
Common metrics: database query time, cache lookup, external API calls, template rendering. Include what helps diagnose performance issues.
Is Server-Timing secure?
Metrics are visible to clients. Avoid exposing sensitive information. Use Timing-Allow-Origin to control cross-origin access to timing data.