HTTP

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.

8 min read intermediate Try in Playground

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+

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.

Keep Learning