Designing CDN Performance SLAs: Metrics, Measurement Points, and Enforcement

Back in 2018, I was on a joint post-mortem call where a retailer insisted we'd missed our 100ms TTFB guarantee for an entire week. Their data came from a single synthetic probe in Virginia hitting an uncached URL. Our edge logs showed a global p95 of 42ms for cached traffic. We'd both signed the same SLA, but we weren't measuring the same thing — and that gap cost us six months of trust and a lot of credits we didn't actually owe.

Why Most Performance SLAs Collapse Under Scrutiny

I've reviewed probably fifty CDN contracts over the last decade, and the same flaw shows up again and again: the SLA tries to promise a user experience with network language. You'll see guarantees like "99.9% uptime" and "sub-100ms latency" in the same paragraph with no definition of where that latency is measured, what traffic it applies to, or what happens during an origin outage. If you don't nail those details, your SLA design isn't protecting anyone — it's just setting up a future argument.

A solid performance SLA needs four sharp edges: a specific metric, a percentile threshold, a defined population, and an explicit measurement point. Don't promise an average TTFB across all requests; that number hides the pain your worst-off users feel. A customer in São Paulo on a 3G connection doesn't care that your average is 60ms when their checkout takes 800ms. We learned this the hard way after promising a mean latency target that looked great on paper but still generated complaints from our APAC users. Now we don't sign anything that isn't percentile-based and region-segmented.

You also have to decide what doesn't count. Every enforceable CDN guarantee I've seen carves out origin failures, client aborts, and force majeure, but the best ones also exclude the first 24 hours after a major purge or config change. If you don't, you're effectively guaranteeing performance during your own customer's most volatile operations. Be explicit about the aggregation window too. A 5-minute window will catch short spikes that a 1-hour window smooths away, and that changes whether you pay credits or not.

# Example: Enforceable performance SLA clause structure
# Don't use averages - define percentile, scope, and exclusions
sla_guarantee:
  name: "Edge TTFB p95"
  metric: "time_to_first_byte"
  aggregation: "p95"
  threshold_ms: 120
  scope:
    traffic: "cache_hit_only"  # misses are origin-dependent
    regions: ["us-east", "eu-west"]
    measurement_points: "edge_pop_egress"
  window: "5m_rolling"
  exclusions: ["origin_5xx", "client_network_error", "scheduled_purge"]
  remedy: "10% credit per 0.1% breach, max 50%"

Picking Metrics That Reflect What Users Actually Feel

For a performance SLA, not all metrics are equal. Cache hit ratio is important for your cost, but it doesn't directly describe user experience. TTFB does, but only if you understand what it includes. As detailed in the HTTP Working Group Specifications, TTFB at the edge is fundamentally different from TTFB as measured by a browser. At the edge you see time to generate headers; at the browser you see that plus network propagation and last-mile latency. If your SLA guarantees one but your customer measures the other, you'll never agree.

Throughput-based guarantees are even trickier. I once saw an SLA that promised "10 Gbps sustained" without defining the file size or TCP stack. A single small-object request will never hit that, no matter how good your network is, because of how Bandwidth-Delay Product and CDN Tuning: Filling the Pipe Without Overwhelming It limits performance on high-latency paths. And if the client's TCP window scaling isn't configured properly, you'll be throttled long before the CDN is saturated. That's why any throughput SLA should reference the test conditions, including object size, concurrency, and transport settings like those discussed in TCP Window Scaling and Its Impact on CDN Throughput: Why Defaults Fall Short. Without that context, you're guaranteeing a car can go 150 mph while it's stuck in first gear.

For intermediate-level web.dev Articles on Core Web Vitals, you'll notice they rely heavily on percentiles for the same reason. We typically structure our metrics like this: TTFB p95 for edge responsiveness, 95th percentile download time for 1MB objects for throughput health, and availability defined as successful edge responses (2xx + 3xx + stale-while-revalidate) divided by total edge requests. Don't mix origin errors into that availability number — it punishes the CDN for your customer's backend failures. And always specify whether you're using Real User Monitoring (RUM) or synthetic probes. RUM shows real pain; synthetic gives you controlled, reproducible numbers. They rarely match.

// Pseudocode for SLA compliance calculation (p95 over 5m window)
function isBreach(requests_5m_window, threshold_ms = 120) {
  // requests: array of { ttfb_ms, is_hit, region }
  const eligible = requests_5m_window.filter(r => 
    r.is_hit && r.region in ['us-east', 'eu-west']
  );
  if (eligible.length < 1000) return false; // not enough sample

  eligible.sort((a, b) => a.ttfb_ms - b.ttfb_ms);
  const p95_index = Math.floor(eligible.length * 0.95);
  const p95_value = eligible[p95_index].ttfb_ms;

  // Consult RTT measurement methodology to avoid last-mile bias
  // See: RTT Measurement Methodology for CDN: Getting Accurate Round-Trip Times
  return p95_value > threshold_ms;
}

Where You Measure Changes Everything

The single biggest fight I've had in SLA enforcement wasn't about the number — it was about the probe. The customer used a third-party tool that measured from a data center in Ashburn, Virginia, to our PoP in Ashburn. Of course the latency looked fantastic. Our RUM data from actual mobile users in the same city was 3x higher because of last-mile variability. Conversely, another client measured from their office behind a congested firewall and blamed us for 400ms of added latency that disappeared the moment we tested from a clean network. If you don't lock down measurement points, you don't have an SLA.

There are three common places to measure, and each tells a different story. Edge egress logs show what your CDN actually did, independent of the internet. Eyeball synthetic probes (like Catchpoint or ThousandEyes) add the network path but in a controlled way. RUM measured in the browser via Navigation Timing shows the truth users feel, including device and local network noise. For a meaningful discussion on how to get clean network numbers, I always point teams to RTT Measurement Methodology for CDN: Getting Accurate Round-Trip Times — if your RTT methodology is sloppy, your TTFB SLA is meaningless.

My rule of thumb after a dozen negotiations: guarantee what you control at the edge, report what the user feels via RUM, and use synthetic as the neutral referee. Your SLA credit should trigger on edge-measured metrics (because that's what you can fix), but your business review should focus on RUM. And never, ever accept a measurement point you don't have audit access to. If you can't see the raw probe data, you can't dispute a false breach.

Metric Primary Measurement Point Typical SLA Target (Intermediate Tier) What It Hides If Misused
Edge TTFB p95 (Cache Hit) PoP egress log timestamp ≤ 75ms (NA/EU), ≤ 120ms (APAC/LATAM) Last-mile latency; looks better than RUM by 40-80ms
Download Time p95 (1MB object) Synthetic probe - eyeball network ≤ 900ms on 20Mbps link Origin fetch time if cache miss not excluded
Availability (Edge Success) Edge status codes / total edge requests ≥ 99.95% per 5-min window Origin 5xx counted as CDN failure
Cache Hit Ratio Edge logs (requests served from cache / total) ≥ 92% (static assets only) Dynamic API traffic dragging ratio down unfairly
Revalidation Latency (stale-while-revalidate) Browser RUM (Navigation Timing) ≤ 150ms p95 for stale serve Synchronous origin revalidation spikes

Building an Enforceable SLA Pipeline: From Probes to Payouts

Here's where most SLAs die in practice — not in the contract language, but in the plumbing. You can't enforce what you can't prove, and you can't prove anything with a single ThousandEyes agent in Ashburn. A defensible pipeline needs at least three independent measurement planes: synthetic probes from eyeball networks, RUM beacons from real users, and passive edge logs. We've learned the hard way that relying on just one gives the vendor an easy out. Synthetics are great for coverage, but they don't see last-mile congestion. RUM shows what users feel, but it's noisy as hell without proper filtering.

The trick is aggregation done right. Don't let anyone sell you on monthly averages — they'll hide a week of 400ms p95 behind three good weeks. Insist on 5-minute buckets evaluated over a rolling 30-day window, with at least 10,000 samples per PoP per bucket to be statistically valid. And filter ruthlessly: strip out origin 5xx, client aborts, and any request with < 1KB body if you're measuring cache performance. You want your SLA to measure the CDN, not your origin falling over. For latency, we typically write it as p95 TTFB < 120ms for cache HITs, p95 < 250ms for cache MISS, measured from the probe's TCP connect start. If you don't separate HIT and MISS, you're not measuring the edge — you're measuring your origin.

Finally, make the data mutually auditable. Both sides should have access to the raw bucket data in near real-time, not a PDF at month's end. We push all SLA metrics to a shared S3 bucket with 1-hour delay and sign every bucket with a hash. That way, when credits are due, there's no "our data shows something different" dance. It also forces you to define exclusions up front — maintenance windows, force majeure, or anycast reconvergence events longer than 90 seconds.

// Enforceable SLA definition - versioned and hashed
{
  "sla_id": "edge-ttfb-p95-hit-v2.1",
  "metric": "ttfb_ms",
  "filter": "cache_status = HIT AND object_size BETWEEN 10KB AND 1MB",
  "objective": {
    "p50_target_ms": 45,
    "p95_target_ms": 120,
    "p99_target_ms": 350
  },
  "measurement_points": [
    "synthetic_eyeball_probes:1600+ ASNs",
    "rum_beacon_sample:5_percent",
    "edge_log_passive"
  ],
  "aggregation": {
    "bucket": "5m",
    "evaluation_window": "rolling_30d",
    "min_samples_per_bucket": 10000,
    "breach_threshold": "p95 > 120ms for >0.3% of buckets"
  },
  "exclusions": ["origin_5xx", "client_rst", "scheduled_maintenance_48h_notice"],
  "remedy": {
    "0.3-1% breach": "10% credit",
    "1-5% breach": "25% credit",
    ">5% breach": "50% credit + termination right"
  }
}

Advanced Enforcement: Taming Anycast, Jitter, and the Long Tail

Once your pipeline's solid, the subtle killers show up. Anycast is the first one to bite you. Your probe in São Paulo might hit Miami one minute and Buenos Aires the next after a BGP flap, and suddenly your p95 jumps 80ms and the vendor claims "internet weather." That's why you must pin measurement to the serving PoP, not just the probe location. Tag every sample with the edge PoP ID from the response header (e.g., x-edge-pop: gru02) and enforce per-PoP SLAs. If gru02 is flapping, you want that visible, not averaged away into a regional number. We also require a 90-second anycast reconvergence exclusion — anything longer is the CDN's routing problem, not yours.

Then there's what happens between p95 and p99. If your p50 looks pristine at 38ms but your p95 is 180ms and p99 is 650ms, you don't have a latency problem — you have a tail-latency problem, and users absolutely feel it. This is where you need to dig into CDN Percentile Latency Analysis: What p50, p95, and p99 Actually Tell You before you set thresholds. A common mistake is to set a p99 target that's just p95 + 20%. In reality, on a congested edge, p99 can be 3-4x p95. We don't enforce p99 as a hard SLA — we use it as an error budget trigger. If p99 exceeds 400ms for more than 0.5% of buckets, it triggers a joint root-cause review, even if p95 is still green.

The last two gremlins are jitter and reordering, and they won't show up in simple TTFB averages at all. CDN Jitter Measurement and Its Impact on User Experience matters because a 60ms mean with 40ms jitter feels far worse for video and API calls than a stable 80ms. We add a jitter SLA for our live and WebSocket workloads: 95% of consecutive samples must stay within ±25ms. And don't ignore Packet Reordering Effects on CDN Performance: The Hidden Throughput Killer — we've seen a single misconfigured load balancer cause 2-3% packet reordering, which collapsed throughput by 30% due to TCP fast-retransmits, while latency SLAs stayed green. If you're pushing large objects, add a throughput or reordering metric alongside TTFB, or you'll miss the actual user pain.

Frequently Asked Questions

Should we use RUM or synthetic for SLA enforcement?

Use both, but for different reasons. Enforce financially on synthetic from eyeball networks — it's controlled, reproducible, and auditable. Use RUM as a secondary signal and a tie-breaker. If synthetic is green but RUM p95 is 200ms higher for three days, that's your early warning that your probe coverage is missing real user pain, and it should trigger a coverage review, not an immediate credit.

What's a realistic TTFB target for a global SLA?

It depends on HIT vs. MISS. For cache HITs on objects 10KB-1MB, we see well-run CDNs hit 35-55ms p50 and 110-140ms p95 globally when measured from eyeball probes. Don't ask for 50ms p95 worldwide — you'll either pay a fortune or get a vendor who games the measurement. Set regional targets instead: <90ms p95 in NA/EU, <140ms p95 in LATAM/APAC.

How do you handle SLA credits without endless arguments?

Automate it. Define credit tiers in code (e.g., 10% for 0.3-1% bucket breach, 25% for 1-5%) and calculate them from the shared bucket data monthly. No human judgment involved. The contract should state that if either party's data differs by >2%, you fall back to a neutral third-party — we use Catchpoint and SpeedCurve as mutual auditors.

Can we SLA throughput instead of just latency?

Yes, and you should for video or large file delivery. Define it as "time-to-last-byte for a 10MB object" or "sustained throughput >40 Mbps for 95% of 30-second sessions." But don't replace latency SLAs with throughput — they catch different failures. Throughput SLAs catch reordering and congestion; latency SLAs catch routing and cache efficiency.

Related Articles

References & Standards: HTTP Working Group Specifications · web.dev Articles · IETF Datatracker