Latency Budgets as an Architectural Constraint
Latency Budgets as an Architectural Constraint
How to allocate latency across API, database, edge, and hydration. When your total budget is 200ms and you have six systems in the critical path, every millisecond allocation is an architectural decision.
The Latency Budget Concept
A latency budget is the total time you can spend before user experience degrades. This isn't arbitrary—it's based on perception research:
┌─────────────────────────────────────────────────────────────────┐
│ User Perception Thresholds │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 0-100ms │ Instant - feels like direct manipulation │
│ 100-300ms │ Slight delay - still feels responsive │
│ 300-1000ms │ Noticeable delay - user perceives "loading" │
│ 1000-3000ms │ User attention wavers - may abandon │
│ 3000ms+ │ Flow broken - significant abandonment risk │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Web Vitals Thresholds: │
│ │
│ TTFB (Time to First Byte) │
│ └── Good: <800ms | Needs Improvement: <1800ms | Poor: >1800ms │
│ │
│ FCP (First Contentful Paint) │
│ └── Good: <1800ms | Needs Improvement: <3000ms | Poor: >3000ms │
│ │
│ LCP (Largest Contentful Paint) │
│ └── Good: <2500ms | Needs Improvement: <4000ms | Poor: >4000ms │
│ │
│ INP (Interaction to Next Paint) │
│ └── Good: <200ms | Needs Improvement: <500ms | Poor: >500ms │
│ │
└─────────────────────────────────────────────────────────────────┘
From these thresholds, derive budgets:
- Page Load Budget: 2500ms (LCP target)
- Interaction Budget: 200ms (INP target)
- Navigation Budget: 300ms (perceived instant transition)
Anatomy of a Request
Decompose where time actually goes:
┌─────────────────────────────────────────────────────────────────┐
│ Full Page Load Latency Breakdown │
├─────────────────────────────────────────────────────────────────┤
│ │
│ User clicks link │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ DNS Lookup │ 20-120ms (uncached) │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ TCP Connection │ 30-100ms (RTT dependent) │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ TLS Handshake │ 30-100ms (1-2 RTT) │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Request to Edge │ 1-50ms (geographic) │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Edge Processing │ 5-50ms (middleware, auth) │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Origin Fetch │ 50-200ms (if cache miss) │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Server Render │ 50-500ms (SSR/RSC) │
│ │ ├─ Data Fetch │ 20-200ms │
│ │ ├─ DB Queries │ 10-100ms │
│ │ └─ React Render │ 20-200ms │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Response Send │ 10-100ms (TTFB variance) │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Asset Download │ 50-500ms (JS, CSS, images) │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Parse & Execute │ 50-300ms (JS main thread) │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Hydration │ 50-500ms (React reconciliation) │
│ └────────┬────────┘ │
│ ▼ │
│ Page Interactive │
│ │
│ Total: 400-2500ms+ (varies wildly by conditions) │
│ │
└─────────────────────────────────────────────────────────────────┘
Budget Allocation Framework
Step 1: Define Your Total Budget
// Performance budget configuration
interface LatencyBudget {
total: number; // Total acceptable latency
breakdown: {
network: number; // DNS, TCP, TLS, RTT
edge: number; // Edge function processing
origin: number; // Origin server processing
render: number; // Server-side rendering
transfer: number; // Response transfer
parse: number; // JS parsing and execution
hydration: number; // React hydration
};
p50Target: number; // Median target
p95Target: number; // Tail latency target
p99Target: number; // Extreme tail target
}
const PAGE_LOAD_BUDGET: LatencyBudget = {
total: 2500, // LCP target
breakdown: {
network: 300, // 12% - largely fixed by physics
edge: 50, // 2% - controllable
origin: 100, // 4% - controllable (cache hit)
render: 200, // 8% - controllable
transfer: 200, // 8% - controllable (compression)
parse: 150, // 6% - controllable (bundle size)
hydration: 300, // 12% - controllable
},
// Leaves 1200ms buffer for variance and LCP element loading
p50Target: 1500,
p95Target: 2500,
p99Target: 4000,
};
const INTERACTION_BUDGET: LatencyBudget = {
total: 200, // INP target
breakdown: {
network: 0, // Local interaction
edge: 0, // No edge for interactions
origin: 50, // API call (if needed)
render: 0, // No SSR
transfer: 20, // Response size
parse: 0, // Already parsed
hydration: 0, // Already hydrated
},
// 130ms for React render + DOM updates
p50Target: 100,
p95Target: 200,
p99Target: 500,
};
Step 2: Measure Current State
// Server-side timing
import { performance } from 'perf_hooks';
export function withTiming<T>(
name: string,
fn: () => Promise<T>
): () => Promise<{ result: T; timing: number }> {
return async () => {
const start = performance.now();
const result = await fn();
const timing = performance.now() - start;
return { result, timing };
};
}
// Request timing middleware
export async function timingMiddleware(
request: Request,
next: () => Promise<Response>
): Promise<Response> {
const timings: Record<string, number> = {};
const start = performance.now();
// Track each phase
const response = await next();
timings.total = performance.now() - start;
// Add Server-Timing header
const serverTiming = Object.entries(timings)
.map(([name, duration]) => `${name};dur=${duration.toFixed(2)}`)
.join(', ');
const headers = new Headers(response.headers);
headers.set('Server-Timing', serverTiming);
return new Response(response.body, {
status: response.status,
headers,
});
}
// Detailed server timing
export class RequestTimer {
private timings: Map<string, { start: number; end?: number }> = new Map();
start(phase: string): void {
this.timings.set(phase, { start: performance.now() });
}
end(phase: string): number {
const timing = this.timings.get(phase);
if (!timing) throw new Error(`Phase ${phase} not started`);
timing.end = performance.now();
return timing.end - timing.start;
}
getHeader(): string {
return Array.from(this.timings.entries())
.filter(([, t]) => t.end !== undefined)
.map(([name, t]) => `${name};dur=${(t.end! - t.start).toFixed(2)}`)
.join(', ');
}
}
// Usage in API route
export async function GET(request: Request) {
const timer = new RequestTimer();
timer.start('auth');
const user = await authenticate(request);
timer.end('auth');
timer.start('db');
const data = await fetchData(user.id);
timer.end('db');
timer.start('serialize');
const json = JSON.stringify(data);
timer.end('serialize');
return new Response(json, {
headers: {
'Content-Type': 'application/json',
'Server-Timing': timer.getHeader(),
},
});
}
Step 3: Client-Side Measurement
// Real User Monitoring (RUM)
class PerformanceObserver {
private metrics: Map<string, number[]> = new Map();
observe(): void {
// Navigation timing
this.observeNavigation();
// Long tasks
this.observeLongTasks();
// Layout shifts
this.observeLayoutShifts();
// Largest Contentful Paint
this.observeLCP();
// First Input Delay / INP
this.observeInteractions();
}
private observeNavigation(): void {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const nav = entry as PerformanceNavigationTiming;
this.record('dns', nav.domainLookupEnd - nav.domainLookupStart);
this.record('tcp', nav.connectEnd - nav.connectStart);
this.record('ttfb', nav.responseStart - nav.requestStart);
this.record('download', nav.responseEnd - nav.responseStart);
this.record('domParse', nav.domContentLoadedEventEnd - nav.responseEnd);
}
});
observer.observe({ entryTypes: ['navigation'] });
}
private observeLongTasks(): void {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
this.record('longTask', entry.duration);
console.warn(`Long task detected: ${entry.duration}ms`, entry);
}
}
});
observer.observe({ entryTypes: ['longtask'] });
}
private observeLCP(): void {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.record('lcp', lastEntry.startTime);
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
}
private observeInteractions(): void {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// INP considers all interactions
this.record('interaction', entry.duration);
}
});
observer.observe({ entryTypes: ['event'], buffered: true });
}
private record(metric: string, value: number): void {
if (!this.metrics.has(metric)) {
this.metrics.set(metric, []);
}
this.metrics.get(metric)!.push(value);
}
getPercentile(metric: string, percentile: number): number {
const values = this.metrics.get(metric) ?? [];
if (values.length === 0) return 0;
const sorted = [...values].sort((a, b) => a - b);
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
return sorted[index];
}
report(): void {
const report = {
timestamp: Date.now(),
url: window.location.href,
metrics: {
ttfb: {
p50: this.getPercentile('ttfb', 50),
p95: this.getPercentile('ttfb', 95),
},
lcp: {
p50: this.getPercentile('lcp', 50),
p95: this.getPercentile('lcp', 95),
},
interaction: {
p50: this.getPercentile('interaction', 50),
p95: this.getPercentile('interaction', 95),
},
},
};
navigator.sendBeacon('/api/rum', JSON.stringify(report));
}
}
Budget Enforcement Strategies
Database Query Budgets
// Prisma middleware for query timing
import { Prisma } from '@prisma/client';
const QUERY_BUDGET_MS = 50; // 50ms per query
export const queryTimingMiddleware: Prisma.Middleware = async (
params,
next
) => {
const start = performance.now();
const result = await next(params);
const duration = performance.now() - start;
// Log slow queries
if (duration > QUERY_BUDGET_MS) {
console.warn(`Slow query detected`, {
model: params.model,
action: params.action,
duration: duration.toFixed(2),
budget: QUERY_BUDGET_MS,
args: JSON.stringify(params.args).slice(0, 200),
});
// Track for alerting
metrics.increment('db.slow_query', {
model: params.model,
action: params.action,
});
}
// Add timing to context
metrics.histogram('db.query_duration', duration, {
model: params.model,
action: params.action,
});
return result;
};
// Query with timeout
async function queryWithTimeout<T>(
query: Promise<T>,
timeoutMs: number
): Promise<T> {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Query timeout')), timeoutMs)
);
return Promise.race([query, timeout]);
}
// Usage
const products = await queryWithTimeout(
db.products.findMany({
where: { category: 'electronics' },
take: 20,
}),
50 // 50ms budget
);
API Route Budgets
// Budget enforcement middleware
interface RouteBudget {
total: number;
phases: {
auth: number;
validation: number;
business: number;
serialization: number;
};
}
const routeBudgets: Record<string, RouteBudget> = {
'/api/products': {
total: 100,
phases: { auth: 10, validation: 5, business: 70, serialization: 15 },
},
'/api/checkout': {
total: 500, // More complex operation
phases: { auth: 20, validation: 20, business: 400, serialization: 60 },
},
};
export function withBudget(
route: string,
handler: (req: Request, budget: BudgetTracker) => Promise<Response>
) {
const config = routeBudgets[route];
return async (req: Request): Promise<Response> => {
const budget = new BudgetTracker(config);
try {
const response = await handler(req, budget);
// Add budget headers
const headers = new Headers(response.headers);
headers.set('X-Budget-Total', String(config.total));
headers.set('X-Budget-Used', String(budget.used));
headers.set('X-Budget-Remaining', String(budget.remaining));
if (budget.exceeded) {
headers.set('X-Budget-Exceeded', 'true');
console.warn(`Route budget exceeded: ${route}`, budget.report());
}
return new Response(response.body, {
status: response.status,
headers,
});
} catch (error) {
if (error instanceof BudgetExceededError) {
return new Response('Service timeout', { status: 503 });
}
throw error;
}
};
}
class BudgetTracker {
private startTime = performance.now();
private phaseTimings: Map<string, number> = new Map();
constructor(private config: RouteBudget) {}
get elapsed(): number {
return performance.now() - this.startTime;
}
get used(): number {
return this.elapsed;
}
get remaining(): number {
return Math.max(0, this.config.total - this.elapsed);
}
get exceeded(): boolean {
return this.elapsed > this.config.total;
}
async phase<T>(name: keyof RouteBudget['phases'], fn: () => Promise<T>): Promise<T> {
const phaseBudget = this.config.phases[name];
const phaseStart = performance.now();
const result = await Promise.race([
fn(),
new Promise<never>((_, reject) =>
setTimeout(
() => reject(new BudgetExceededError(`Phase ${name} exceeded budget`)),
phaseBudget
)
),
]);
this.phaseTimings.set(name, performance.now() - phaseStart);
return result;
}
report(): Record<string, unknown> {
return {
total: this.config.total,
used: this.used,
exceeded: this.exceeded,
phases: Object.fromEntries(this.phaseTimings),
};
}
}
// Usage
export const GET = withBudget('/api/products', async (req, budget) => {
const user = await budget.phase('auth', () => authenticate(req));
const params = await budget.phase('validation', () => parseParams(req));
const products = await budget.phase('business', () =>
db.products.findMany({
where: params.filters,
take: params.limit,
})
);
const json = await budget.phase('serialization', () =>
JSON.stringify(products)
);
return new Response(json, {
headers: { 'Content-Type': 'application/json' },
});
});
Render Budgets
// React Server Component render budget
import { cache } from 'react';
const RENDER_BUDGET_MS = 200;
export const measureRender = cache(
<T>(name: string, fn: () => T): T => {
const start = performance.now();
const result = fn();
const duration = performance.now() - start;
if (duration > 50) {
console.warn(`Slow render: ${name} took ${duration.toFixed(2)}ms`);
}
return result;
}
);
// Component-level budgets
interface ComponentBudget {
dataFetch: number;
render: number;
children: number;
}
async function BudgetedComponent({
budget,
children,
}: {
budget: ComponentBudget;
children: React.ReactNode;
}) {
const tracker = new BudgetTracker({
total: budget.dataFetch + budget.render + budget.children,
phases: {
auth: 0,
validation: 0,
business: budget.dataFetch,
serialization: budget.render,
},
});
// This is conceptual - actual implementation would use React profiler
return children;
}
Hydration Budget Management
Hydration is often the largest controllable latency source:
┌─────────────────────────────────────────────────────────────────┐
│ Hydration Cost Breakdown │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Typical Hydration Timeline (200KB JS bundle): │
│ │
│ 0ms │ HTML parsed, React bundle starts loading │
│ 100ms │ React bundle downloaded │
│ 150ms │ JS parsing complete │
│ 180ms │ React initialization │
│ 250ms │ Hydration starts - DOM reconciliation │
│ 400ms │ Event handlers attached │
│ 450ms │ First interaction possible │
│ │
│ Hydration Cost Factors: │
│ • Bundle size (parsing time) │
│ • Component tree depth │
│ • Number of DOM nodes │
│ • Event handler count │
│ • useEffect callbacks │
│ • Initial state computation │
│ │
└─────────────────────────────────────────────────────────────────┘
Progressive Hydration
// Lazy hydration for non-critical components
import { lazy, Suspense } from 'react';
// Defer hydration until visible
function LazyHydrate({
children,
whenVisible = false,
whenIdle = false,
on = [],
}: {
children: React.ReactNode;
whenVisible?: boolean;
whenIdle?: boolean;
on?: ('click' | 'hover' | 'focus')[];
}) {
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
if (hydrated) return;
// Hydrate when visible
if (whenVisible) {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setHydrated(true);
observer.disconnect();
}
});
observer.observe(containerRef.current!);
return () => observer.disconnect();
}
// Hydrate when idle
if (whenIdle) {
const id = requestIdleCallback(() => setHydrated(true));
return () => cancelIdleCallback(id);
}
// Hydrate on interaction
if (on.length > 0) {
const handler = () => setHydrated(true);
on.forEach((event) => {
containerRef.current?.addEventListener(event, handler, { once: true });
});
}
}, [whenVisible, whenIdle, on, hydrated]);
// Render static HTML until hydrated
if (!hydrated) {
return <div ref={containerRef}>{children}</div>;
}
return children;
}
// Usage
function ProductPage() {
return (
<>
{/* Critical - hydrate immediately */}
<ProductHeader />
<AddToCartButton />
{/* Defer until visible */}
<LazyHydrate whenVisible>
<ProductReviews />
</LazyHydrate>
{/* Defer until idle */}
<LazyHydrate whenIdle>
<RecommendedProducts />
</LazyHydrate>
{/* Defer until interaction */}
<LazyHydrate on={['click', 'hover']}>
<ProductQuestions />
</LazyHydrate>
</>
);
}
Selective Hydration with Islands
// Island architecture - only hydrate interactive parts
import { createIsland } from './island-framework';
// Static component - no JS shipped
function ProductDescription({ html }: { html: string }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
// Interactive island - hydrated
const AddToCartIsland = createIsland(
() => import('./AddToCartButton'),
{
loading: <button disabled>Add to Cart</button>,
hydrationStrategy: 'visible', // or 'idle', 'interaction'
}
);
// Page composition
function ProductPage({ product }: { product: Product }) {
return (
<div>
{/* Static - 0 JS */}
<h1>{product.name}</h1>
<ProductDescription html={product.descriptionHtml} />
<ProductImages images={product.images} />
{/* Islands - minimal JS */}
<AddToCartIsland productId={product.id} />
<WishlistIsland productId={product.id} />
{/* Static footer - 0 JS */}
<ProductSpecs specs={product.specs} />
</div>
);
}
// Island framework implementation
function createIsland<P>(
loader: () => Promise<{ default: React.ComponentType<P> }>,
options: IslandOptions
) {
return function Island(props: P) {
const [Component, setComponent] = useState<React.ComponentType<P> | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let mounted = true;
const hydrate = async () => {
const { default: Comp } = await loader();
if (mounted) setComponent(() => Comp);
};
switch (options.hydrationStrategy) {
case 'immediate':
hydrate();
break;
case 'idle':
requestIdleCallback(hydrate);
break;
case 'visible':
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
hydrate();
observer.disconnect();
}
});
observer.observe(containerRef.current!);
break;
case 'interaction':
const handler = () => hydrate();
containerRef.current?.addEventListener('click', handler, { once: true });
containerRef.current?.addEventListener('mouseenter', handler, { once: true });
break;
}
return () => {
mounted = false;
};
}, []);
if (!Component) {
return <div ref={containerRef}>{options.loading}</div>;
}
return <Component {...props} />;
};
}
Latency Budget Dashboard
// Real-time budget monitoring
interface LatencyMetrics {
route: string;
timestamp: number;
budget: number;
actual: {
p50: number;
p95: number;
p99: number;
};
breakdown: {
network: number;
edge: number;
origin: number;
render: number;
hydration: number;
};
violations: number; // Count of budget exceeded
}
class LatencyBudgetMonitor {
private metrics: Map<string, LatencyMetrics[]> = new Map();
private budgets: Map<string, number> = new Map();
setBudget(route: string, budgetMs: number): void {
this.budgets.set(route, budgetMs);
}
record(route: string, timing: TimingData): void {
const budget = this.budgets.get(route) ?? 2500;
const metrics = this.metrics.get(route) ?? [];
metrics.push({
route,
timestamp: Date.now(),
budget,
actual: timing,
breakdown: timing.breakdown,
violations: timing.p95 > budget ? 1 : 0,
});
// Keep last hour
const oneHourAgo = Date.now() - 3600000;
this.metrics.set(
route,
metrics.filter((m) => m.timestamp > oneHourAgo)
);
}
getReport(route: string): BudgetReport {
const metrics = this.metrics.get(route) ?? [];
const budget = this.budgets.get(route) ?? 2500;
if (metrics.length === 0) {
return { route, budget, status: 'unknown', samples: 0 };
}
const p95s = metrics.map((m) => m.actual.p95);
const avgP95 = p95s.reduce((a, b) => a + b, 0) / p95s.length;
const violations = metrics.filter((m) => m.violations > 0).length;
const violationRate = violations / metrics.length;
return {
route,
budget,
status: violationRate > 0.05 ? 'critical' : violationRate > 0.01 ? 'warning' : 'healthy',
samples: metrics.length,
avgP95,
violationRate,
breakdown: this.aggregateBreakdown(metrics),
recommendation: this.generateRecommendation(metrics, budget),
};
}
private aggregateBreakdown(metrics: LatencyMetrics[]): Record<string, number> {
const totals: Record<string, number[]> = {};
for (const m of metrics) {
for (const [phase, value] of Object.entries(m.breakdown)) {
if (!totals[phase]) totals[phase] = [];
totals[phase].push(value);
}
}
return Object.fromEntries(
Object.entries(totals).map(([phase, values]) => [
phase,
values.reduce((a, b) => a + b, 0) / values.length,
])
);
}
private generateRecommendation(
metrics: LatencyMetrics[],
budget: number
): string[] {
const breakdown = this.aggregateBreakdown(metrics);
const recommendations: string[] = [];
// Find the biggest contributors
const sorted = Object.entries(breakdown).sort(([, a], [, b]) => b - a);
for (const [phase, avgTime] of sorted) {
const percentage = (avgTime / budget) * 100;
if (percentage > 30) {
recommendations.push(
`${phase} is consuming ${percentage.toFixed(0)}% of budget (${avgTime.toFixed(0)}ms avg). Consider optimization.`
);
}
}
return recommendations;
}
}
// Dashboard API
export async function GET(request: Request) {
const monitor = getMonitor();
const routes = ['/products', '/product/[id]', '/checkout', '/api/cart'];
const reports = routes.map((route) => monitor.getReport(route));
return Response.json({
timestamp: new Date().toISOString(),
routes: reports,
summary: {
healthy: reports.filter((r) => r.status === 'healthy').length,
warning: reports.filter((r) => r.status === 'warning').length,
critical: reports.filter((r) => r.status === 'critical').length,
},
});
}
Budget Allocation by Route Type
┌─────────────────────────────────────────────────────────────────┐
│ Recommended Budget Allocations │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Static Marketing Page (Target: 1500ms LCP) │
│ ├── Network: 200ms (13%) │
│ ├── Edge: 20ms (1%) │
│ ├── CDN/Cache: 10ms (1%) ← Should be cache hit │
│ ├── Transfer: 100ms (7%) │
│ ├── Parse: 100ms (7%) │
│ └── Paint: 200ms (13%) │
│ Buffer: 870ms (58%) │
│ │
│ Dynamic Product Page (Target: 2500ms LCP) │
│ ├── Network: 300ms (12%) │
│ ├── Edge: 50ms (2%) │
│ ├── Origin: 200ms (8%) ← SSR with data fetch │
│ ├── Transfer: 150ms (6%) │
│ ├── Parse: 200ms (8%) │
│ ├── Hydration: 400ms (16%) │
│ └── LCP Load: 300ms (12%) │
│ Buffer: 900ms (36%) │
│ │
│ Checkout Flow (Target: 500ms per step) │
│ ├── API Call: 150ms (30%) │
│ ├── Validation: 50ms (10%) │
│ ├── Processing: 200ms (40%) │
│ └── UI Update: 100ms (20%) │
│ │
│ Interactive Dashboard (Target: 200ms INP) │
│ ├── Event: 10ms (5%) │
│ ├── JS Execute: 50ms (25%) │
│ ├── React: 80ms (40%) │
│ └── DOM/Paint: 60ms (30%) │
│ │
└─────────────────────────────────────────────────────────────────┘
Summary
Latency budgets turn performance from an afterthought into an architectural constraint:
- Define total budgets from user perception thresholds (100ms instant, 2500ms LCP)
- Decompose into phases — network, edge, origin, render, hydration
- Allocate per phase — each system gets a millisecond budget
- Measure continuously — Server-Timing headers, RUM, percentile tracking
- Enforce at boundaries — timeout queries, fail fast, circuit break
- Alert on violations — budget exceeded = performance regression
The key insight: you can't optimize what you don't measure, and you can't prioritize without a budget. When your database team asks for 100ms queries and your frontend team wants 500ms hydration, the latency budget is the arbiter.
What did you think?