A few years back, we had a major retail customer whose origin was perfectly healthy — 200s across the board, edge cache hit ratio at 98%, yet their support inbox was flooding with "site broken" screenshots. Turns out a bad CSP header we'd pushed at the edge was blocking checkout scripts for about 12% of users, and we didn't see a thing server-side. That's the blind spot that pushed us to adopt the Reporting API, and I've relied on it for client-side monitoring ever since.
How the Reporting API Fills the CDN Visibility Gap
For a decade, our CDN observability story was almost entirely server-centric. We had exquisite data on cache hits, origin latency, edge CPU, and TCP retransmits, but we were blind to what actually happened after we sent the bytes. If a browser blocked a resource due to a CSP violation, failed to negotiate HTTP/3 Connection Migration: Seamless Handoffs Across Network Changes, or hit a deprecation that broke a critical script, that error died on the client. Traditional RUM beacons help, but they only fire if your own JavaScript executes — which it often doesn't when the browser intervenes for security or network reasons.
The Reporting API flips that model. Instead of you trying to catch errors with window.onerror, the browser itself becomes the observer. It watches for a defined set of failures — CSP violations, deprecations, interventions, crash reports, and network errors via NEL — queues them internally, and delivers them out-of-band to an endpoint you specify, even if the page has already navigated away. I've found this incredibly reliable compared to synchronous beacons, because reports are sent with a best-effort, low-priority fetch that doesn't compete with page resources. That separation is crucial; you don't want your error reporting to make performance worse. The behavior is formally defined in the HTTP Working Group Specifications and explained with practical examples over at web.dev Articles.
# Modern syntax (Reporting-Endpoints) + legacy fallback for older browsers
# Sent as HTTP response headers from your CDN edge
Reporting-Endpoints: cdn-monitor="https://reports.edge-collector.example.net/v1/browser", default="https://reports.edge-collector.example.net/v1/default"
Report-To: {"group":"cdn-monitor","max_age":10800,"endpoints":[{"url":"https://reports.edge-collector.example.net/v1/browser"}],"include_subdomains":true}
# Tell the browser which report types to send to that group
Content-Security-Policy: default-src 'self'; report-to cdn-monitor; report-uri https://reports.edge-collector.example.net/v1/default
NEL: {"report_to":"cdn-monitor","max_age":86400,"include_subdomains":true,"success_fraction":0.05,"failure_fraction":1.0}
Cross-Origin-Opener-Policy: same-origin; report-to="cdn-monitor"
Cross-Origin-Embedder-Policy: require-corp; report-to="cdn-monitor"
Configuring Report Delivery Without Flooding Your Collector
When we first rolled this out, we made the rookie mistake of setting failure_fraction: 1.0 and success_fraction: 1.0 for NEL on a high-traffic property doing 50k requests per second. Our collector — a small Go service behind a load balancer — fell over in about 90 seconds. You don't need every success report to get value; sampling is your friend. For most CDN deployments, I now recommend starting with failure_fraction: 1.0 and success_fraction between 0.01 and 0.05, then tuning based on collector capacity and how much baseline you need for error rate denominators.
The other nuance is header delivery at the edge. Don't set these headers at origin if you can avoid it — set them in VCL, Workers, or your edge rule engine so you can vary them by content type, geography, or A/B cohort. For example, we only enable COOP/COEP reporting on our document responses, not on images or video segments, to cut noise. And you'll want two collector endpoints: a dedicated one for your primary group and a fallback default group. Browsers will retry failed report deliveries to the configured endpoints automatically, but they won't retry forever, so make that endpoint highly available and keep its TLS config bulletproof. If the report endpoint itself triggers a CSP or certificate error, you'll lose the very reports you need.
Handling backpressure correctly also matters for HTTP/2 Flow Control Tuning for CDN: Window Sizes and Stream Management at scale. Report uploads are small POSTs, typically 1-4 KB gzipped, but they arrive in bursts after a bad deployment. We front our collector with a discard-friendly queue — reports hit a lightweight edge worker that writes to Kafka and returns 204 immediately. The actual parsing and enrichment happens asynchronously. If you try to do synchronous validation or database writes in the request path, you'll create a bottleneck that looks a lot like a flow-control stall to the browser.
// Example browser report payload POSTed to https://reports.edge-collector.example.net/v1/browser
// Content-Type: application/reports+json
[{
"age": 42,
"type": "csp-violation",
"url": "https://www.example.com/checkout",
"user_agent": "Mozilla/5.0 ... Chrome/124.0.0.0",
"body": {
"blockedURL": "https://payments.thirdparty.net/sdk.js",
"disposition": "enforce",
"documentURL": "https://www.example.com/checkout",
"effectiveDirective": "script-src",
"originalPolicy": "default-src 'self'; script-src 'self' 'nonce-abc123'",
"referrer": "https://www.example.com/cart",
"statusCode": 200
}
},
{
"age": 15,
"type": "nel",
"url": "https://www.example.com/api/cart",
"body": {
"elapsed_time": 1245,
"method": "GET",
"phase": "connection",
"protocol": "h3",
"referrer": "https://www.example.com/checkout",
"sampling_fraction": 0.05,
"server_ip": "203.0.113.42",
"status_code": 0,
"type": "tcp.timed_out"
}
}]
Decoding Report Types: What Actually Matters for CDN Ops
Not all report types carry the same operational weight, and that's where I've seen teams waste time. CSP violation reports are by far the noisiest, but they're also the most actionable for CDN folks. When you push a new CSP or modify a strict-dynamic policy at the edge, you'll know within minutes if you've broken a third-party payment provider or analytics script in a specific region. I tie these directly to our deployment pipeline — if csp-violation volume spikes more than 3 sigma above baseline within 10 minutes of a config push, we auto-rollback.
Network Error Logging (NEL) is the other heavy hitter. Unlike synthetic checks that run from a handful of PoPs, NEL gives you client-perceived network failures segmented by phase — dns, connection, application — and by protocol. This is how we caught a subtle MTU blackhole that only affected HTTP/3 clients on one transit provider in Southeast Asia. Our edge logs showed no errors, but NEL reports showed tcp.timed_out and http.protocol.error clustered on h3. Without that, we'd have chased it for weeks. Pairing these reports with proper HTTP/2 Stream Prioritization at CDN Edges: Delivering Critical Resources First logic helped us confirm the fallback to h2 was working once we mitigated.
The rest — deprecation, intervention, and crash reports — are lower volume but valuable for forward planning. Deprecation reports warned us a full quarter before Chrome removed a prefixed API our customer’s carousel library still used. We passed that warning upstream and saved them an emergency hotfix. Don't ignore them, but don't alert on them with the same urgency as NEL or CSP.
| Report Type (type) | Typical Rate per 1M Page Views | Median Collector Latency | Primary CDN Use Case |
|---|---|---|---|
| csp-violation | 800 - 4,500 (spikes to 50k+ on bad policy) | ~45 sec (browser delay + batching) | Validate CSP/Permissions-Policy pushes, detect mixed-content blocks |
| nel (failure) | 1,200 - 3,000 (highly geography-dependent) | ~60-120 sec | Detect edge unreachable, TCP/TLS timeouts, protocol-specific failures |
| deprecation | 150 - 400 | ~90 sec | Forecast breaking API removals, plan polyfills |
| intervention | 50 - 200 | ~70 sec | Spot browser throttling (e.g., heavy image, slow connection interventions) |
| coop/coep (cross-origin isolation) | 20 - 80 | ~50 sec | Debug popup/frame isolation for high-security properties |
Wiring Up Reporting-Endpoints on the Edge: A Production-Ready Config
You've got the theory — now let's make it survive real traffic. The biggest mistake I see teams make is setting max_age too high on day one and then getting stuck with a bad collector URL cached in 2 million browsers for a week. Don't do it. Start with a short-lived group, validate delivery, then ramp. We've learned to roll this out in three stages: a 1% canary with success_fraction: 0.01, then 10%, then full sampling, each stage holding for at least 24 hours so you catch both mobile and desktop long-tail sessions.
On the CDN side, you want to inject headers at the edge, not the origin. That way you don't bust cache and you can A/B test header values by PoP, device class, or route. Set Reporting-Endpoints as the modern standard and keep Report-To for Safari and older Chromium for another 6-12 months — you'll still see ~8-12% of reports come in via the legacy header. Scope your endpoint to a dedicated ingest subdomain that bypasses your main WAF rules; reports are POST with Content-Type: application/reports+json and they will get false-positive blocked by bot rules if you're not careful. And please, use a separate collector from your analytics beacon — when that collector 502s, you don't want it to take down your Core Web Vitals ingestion too.
# Production edge header injection (Nginx / Varnish VCL / Worker equivalent)
# Keep max_age short initially - 3 hours = 10800s
add_header Reporting-Endpoints 'cdn-reports="https://reports-collector.example.net/v1/cdn-reports", cdn-reports-fallback="https://reports-fallback.example.net/v1/cdn-reports"' always;
add_header Report-To '{"group":"cdn-reports","max_age":10800,"endpoints":[{"url":"https://reports-collector.example.net/v1/cdn-reports","priority":1,"weight":1},{"url":"https://reports-fallback.example.net/v1/cdn-reports","priority":2,"weight":1}],"include_subdomains":true}' always;
# NEL + CSP + COOP/COEP reporting pointed at same group
add_header NEL '{"report_to":"cdn-reports","max_age":86400,"success_fraction":0.05,"failure_fraction":1.0,"include_subdomains":true}' always;
add_header Content-Security-Policy "default-src 'self'; report-to cdn-reports; report-uri https://reports-collector.example.net/v1/cdn-reports;" always;
add_header Cross-Origin-Opener-Policy "same-origin; report-to cdn-reports" always;
# Collector-side: Accept and ack fast. 204, no body, <50ms.
# location /v1/cdn-reports { limit_req zone=reports burst=200 nodelay; return 204; }
Advanced Patterns: Deduplication, Correlation, and Cutting Through the Noise
Once you're ingesting 5k-50k reports per minute, raw volume isn't your problem — noise is. The Reporting API will happily flood you with the same NEL failure from a single stalled client retrying 15 times in 30 seconds. You need to dedupe on the tuple of (client IP /24, report URL host, report type, 5-minute bucket). We've found that cuts volume by ~60-70% without losing signal. The field you really want for correlation isn't the IP though — it's age and the User-Agent plus your edge's Server-Timing or cf-ray / x-cache-id if you echo it. Inject a server-timing: cdn-poP;dur=12, cache;desc=HIT header and you can join reports directly to your edge logs by PoP and cache state, even when the client clock is skewed by minutes.
That correlation is what makes this actually useful for CDN ops versus just browser trivia. For example, when we see a spike in csp-violation or coop reports from a single PoP after a config push, we can instantly check if HTTP Trailers for CDN: Post-Body Checksums, Timing, and Logging are showing truncated bodies or checksum mismatches on that same PoP. Similarly, if you're debugging intermittent failed NEL reports, don't chase them in isolation — pull your edge logs and look for 499s, TLS handshake failures, and origin timeouts in the same window. About 40% of what looks like "client-side network errors" turns out to be Anycast route flaps or origin TCP queue overflows that never hit your 5xx dashboards.
Two more patterns that've saved us. First, use a report sampling strategy tied to your feature flags. If you're experimenting with WebTransport Through CDN Proxies: Bidirectional Streams Over HTTP/3, create a dedicated reporting group webtransport-reports with success_fraction: 1.0 just for that path prefix, so you get full fidelity without paying the cost globally. Second, don't use Reporting API to reinvent what conditional requests already solve. For stale-while-revalidate or cache validation issues, you'll get cleaner data from pairing reports with Advanced HTTP Conditional Requests: If-Match, If-Range, and Partial Updates logging at the edge — ETag mismatches and 412s tell you more than a generic intervention report ever will. And if your collector starts returning 413s? You've set your batch size wrong — browsers will queue up to ~100 reports and send gzipped; keep your ingest limit at 256KB per POST.
Frequently Asked Questions
Why am I getting duplicate reports for the same error?
That's expected. Browsers queue and retry — Chromium will attempt delivery for up to ~60 seconds with exponential backoff, and if the user has multiple tabs open to your origin, each context can fire its own report. Deduplicate on the server using a hash of body.url + body.type + user_agent + /24 subnet within a 5-minute window. Don't try to dedupe client-side; you'll just lose data.
How long should I keep my reporting collector's max_age?
Start at 10800 (3 hours) for the first week. Once you've confirmed <95th percentile delivery latency is under 2 seconds and your fallback endpoint is handling <0.1% of traffic, bump to 86400 (24 hours). We don't recommend going beyond 604800 (7 days) — if you need to rotate collector domains for compliance or DNS changes, you're stuck waiting a week for clients to expire the old endpoint.
Do Reporting API reports work when my assets are served from a third-party CDN hostname?
Yes, but you must set include_subdomains and ensure the header is served from the hostname that actually served the resource. Reports are scoped to the eTLD+1 that set the endpoint. If your HTML is on www.example.com but images are on cdn.example.net, each needs its own Reporting-Endpoints header. Wildcard or cross-site reporting without explicit opt-in won't work, and third-party iframes can't report to your endpoint unless they set it themselves.
Can the Reporting API replace my RUM beacon or synthetic monitoring?
No — it's a complement. RUM tells you how fast things were, synthetics tell you if they're up from 20 global probes, and Reporting API tells you *why* they failed from the client's perspective when everything else says 200 OK. We've seen it catch 15-20% more transport and policy failures that never fire a window.onerror. Keep all three. Budget about 1-3% extra ingest cost for reports versus your existing RUM pipeline.