You're watching a Champions League knockout match, you join 25 minutes late, and you want to rewind to see that early goal everyone’s texting about. If your stream can't do that, you've already lost the viewer. I've seen this exact scenario tank engagement metrics for a major broadcaster back in 2019 — they'd built a fantastic low-latency pipeline but completely forgot about time-shifted viewing. Their DVR window was zero. A proper DVR CDN setup fixes that, and it all happens at the edge without hammering your origin.
How Edge DVR Turns a Live Stream Into a Rewindable Window
At its core, live DVR isn’t magic. It’s just a sliding window of segments that the edge holds onto and re-serves on demand. For HLS, that window is defined by how many .ts or .m4s segments you keep addressable via the manifest. For DASH, it’s the timeShiftBufferDepth in your MPD. The origin keeps generating new segments, but the edge is responsible for keeping the last, say, 2 or 4 hours available and for rewriting the manifest so each viewer gets a consistent, personalized view of that window.
What trips up most teams is where that window actually lives. Early in my career I assumed the origin should store the entire DVR buffer and the edge would just pass it through. That doesn’t scale. Every rewind request would collapse back to origin and you’d melt your storage tier during a big event. The right pattern is to push DVR functionality to the edge cache and shield layer. The origin holds the authoritative archive (often on S3 or equivalent), but the hot, rewindable window — the last 30 minutes to 6 hours — lives in edge RAM and SSD. When a viewer requests playlist.m3u8?timeshift=1800 to go back 30 minutes, the edge can serve the correct manifest version directly and pull only missing segments from shield, not origin. If you're still unifying your packager, you'll want to read CMAF Packaging for CDN Delivery: Unified Streaming Format for HLS and DASH first, since CMAF makes this edge buffering far cleaner with a single segment format.
Manifest manipulation is where the real work happens. You can’t serve every viewer the same live manifest if you support rewind and pause. You need to handle EXT-X-MEDIA-SEQUENCE and EXT-X-PLAYLIST-TYPE correctly, and you must avoid caching a single manifest for all users when query parameters change the window. I’ve debugged too many deployments where the CDN cached one manifest for 2 seconds and every time-shifted viewer got the live edge instead of their requested offset.
# Example VCL / edge logic for DVR window handling (pseudo-VCL)
sub vcl_recv {
# Normalize DVR offset parameter to avoid cache fragmentation
if (req.url ~ "timeshift=") {
set req.http.X-DVR-Offset = regsub(req.url, ".*timeshift=([^&]+).*", "\1");
# Keep DVR manifests separate from live-edge manifests
set req.url = regsub(req.url, "\?.*", "") + "?dvr=" + req.http.X-DVR-Offset;
}
}
sub vcl_backend_response {
# Keep DVR segments longer at edge than live edge segments
if (bereq.url ~ "\.m4s$" || bereq.url ~ "\.ts$") {
# Hot DVR window: 4 hours @ 6s segments = 2400 segments
set beresp.ttl = 4h;
set beresp.grace = 1h;
# Follow HTTP Working Group Specifications for cache validation
# https://httpwg.org/specs/
set beresp.http.Cache-Control = "public, max-age=14400, stale-while-revalidate=3600";
}
if (bereq.url ~ "playlist\.m3u8") {
# Never cache personalized DVR manifests at edge for more than 1 segment duration
set beresp.ttl = 2s;
set beresp.http.Cache-Control = "private, no-cache";
}
}
Catch-Up TV vs. Live DVR: Different Beasts at the Edge
People use catch-up TV CDN and live DVR interchangeably, but they put very different pressure on your CDN. Live DVR — what we just described — is a rolling buffer. Usually 30 minutes to 4 hours, stored entirely at the edge and shield, designed for pause, rewind, and resume during the live event. Catch-up TV is different: it’s the ability to start a 3-hour-old program from the beginning, often after the live event has ended, and it can require days of retention.
For live DVR, your edge needs fast, ephemeral storage. Think NVMe or RAM with a proper eviction policy. For catch-up, you need durable, addressable archiving with origin-assisted revalidation. I worked with a European broadcaster that tried to serve 7-day catch-up entirely from edge SSDs. Their hit ratio looked great for 48 hours, then their eviction rate spiked and every catch-up request became an origin fetch. We moved catch-up to an origin archive with edge caching on first request, and kept only the 2-hour live DVR window pinned at edge. Origin load dropped 68% overnight. The protocol you choose also matters here — the way HLS and DASH handle time-shift signaling isn’t identical, and that affects how you configure your edge packager. See DASH vs HLS for CDN Streaming: Protocol Comparison and Selection Guide for the specifics on timeShiftBufferDepth vs HLS sliding windows.
Don’t forget low-latency when you add DVR. Clients using LL-HLS with 2-second parts expect sub-3-second latency at the live edge, but a viewer who rewound 10 minutes doesn’t need that same latency. Your edge should serve rewind viewers from standard segments, not low-latency parts, to avoid holding fragmented part caches. We typically branch the logic: if timeshift > 0, disable LL-HLS part handling and serve the regular 6-second segment rendition. It simplifies caching and cuts down on 429s from the origin. For the live-edge path, follow the setup in Low-Latency HLS Configuration for CDN: Sub-3-Second Live Streaming and keep it isolated from your DVR path. Helpful background on media delivery performance is also covered in web.dev Articles (https://web.dev/articles/).
# HLS manifest example for a 2-hour DVR window (6s segments)
# Live edge manifest (last 3 segments shown for brevity)
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:4820
#EXT-X-PLAYLIST-TYPE:EVENT
#EXTINF:6.000,
segment_4820.m4s
#EXTINF:6.000,
segment_4821.m4s
#EXTINF:6.000,
segment_4822.m4s
# Time-shifted request: GET /live/playlist.m3u8?timeshift=1800
# Edge rewrites to a window 30 minutes (300 segments) behind live
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:4520
#EXTINF:6.000,
segment_4520.m4s
#EXTINF:6.000,
segment_4521.m4s
#EXTINF:6.000,
segment_4522.m4s
# ... continues up to live edge minus offset
Sizing Your DVR Window: What the Window Costs You
There’s no free rewind. Every minute you add to your DVR window costs you storage, cache keys, and invalidation complexity. For an intermediate setup, I tell teams to start with 2 hours and measure. That’s usually enough for time-shifted viewing during a typical sports match or news block without blowing up your edge footprint. A 2-hour window at 5 Mbps average bitrate with 3 renditions (1080p, 720p, 480p) is roughly 4-5 GB per channel at the shield layer, but because it’s fragmented into thousands of small segments, your overhead for object metadata and cache lookups is the real killer if you’re not careful.
You’ll also want to watch your rebuffer ratio and time-to-first-frame for DVR sessions specifically. Live-edge viewers and DVR viewers behave differently. DVR viewers seek — a lot. Each seek is a manifest refresh plus 2-3 segment fetches that aren’t sequential. If your edge isn’t tuned for that random access, you’ll see higher origin fetches and slower start times for the rewind audience. We instrument DVR and live as separate traffic classes in our CDN analytics for exactly that reason.
| DVR Window Size | Edge Storage per Channel (3 renditions, ~5 Mbps avg) | Median Seek Latency (edge HIT) | Origin Shield Offload |
|---|---|---|---|
| 30 minutes | ~1.1 GB / 300 segments | 85 ms | 98.2% |
| 2 hours | ~4.3 GB / 1,200 segments | 110 ms | 96.5% |
| 4 hours | ~8.6 GB / 2,400 segments | 145 ms | 93.1% |
| 24 hours (catch-up) | ~52 GB / 14,400 segments | 280 ms (often MISS) | 74.8% |
Implementing DVR at the Edge: Manifest Rewriting and Cache Control in Practice
You'll quickly learn that Edge DVR isn't just "cache HLS longer." The origin packager is still the source of truth — it maintains the full DVR window as a rolling buffer of segments and continuously rewrites the manifest. For a 4-hour window with 6-second segments, that's 2,400 .ts or .m4s files plus the updated .m3u8/.mpd sitting on origin. The edge's job isn't to store that window forever; it's to intelligently cache what's been requested and to serve manifest variants that reflect where the viewer is in time.
Here's how we've typically wired it. The origin outputs an EVENT or sliding-window manifest with #EXT-X-MEDIA-SEQUENCE rolling forward and segments addressed by time, not just sequence number. The edge pulls the live manifest with Cache-Control: no-cache, max-age=1-2s — you never want a 30-second cached manifest when someone's rewinding. Segments, though, are immutable once written, so we cache them hard: public, max-age=14400, stale-while-revalidate for a 4h window. If you're designing this from scratch, start with your CDN Live Streaming Architecture: Ingest, Transcode, Package, and Deliver at Scale and add DVR as a packaging + caching layer, not a separate storage system. It'll save you a painful re-architecture later.
# Edge DVR - NGINX + VOD module with cache slicing
# 4-hour window = 2400 segments @ 6s each
location ~ ^/live/(?<channel>[^/]+)/index\.m3u8$ {
vod hls;
vod_mode live;
vod_hls_segment_duration 6000;
vod_live_dvr_window 14400; # seconds - keep 4h rewindable
# Manifest must stay fresh - drift kills rewind
add_header Cache-Control "no-cache, max-age=2";
add_header Access-Control-Allow-Origin "*";
proxy_cache_bypass 1;
proxy_no_cache 1;
}
location ~ ^/live/.*\.(ts|m4s|cmfa|cmfv)$ {
proxy_pass http://origin_packager;
proxy_cache edge_dvr_cache;
proxy_cache_key $scheme$host$uri;
proxy_cache_valid 200 5h; # window + 1h safety buffer
proxy_cache_lock on;
slice 1m; # cache slicing prevents large segment eviction
add_header Cache-Control "public, max-age=14400, immutable";
add_header X-Cache-Status $upstream_cache_status;
}
That proxy_cache_valid 5h is deliberate. Your DVR window is 4 hours, but you want origin segments to linger at the edge for an extra hour to handle late rewinds and origin failover. We've seen hit ratios drop from 96% to 78% when teams set TTL exactly equal to the window — every viewer scrubbing near the edge of the window triggers an origin re-fetch. Give yourself that 20-30% buffer. And always use time-based URLs (/live/channel/1234567890.ts) rather than sequential (seg123.ts) if your packager supports it; it makes cache invalidation and multi-edge consistency far less brittle.
Beyond Basic Rewind: Low-Latency DVR, SSAI Collisions, and Fixing Broken Time-Shift
The first time DVR really breaks is when you try to bolt it onto low-latency. CMAF chunks with 200-500ms parts and a 2-second latency target don't play nicely with a naive 4-hour DVR window. You can't cache 28,800 tiny chunks the same way you cache 2,400 full segments. The pattern that works is dual-track packaging: your Low-Latency DASH with CMAF Chunks: Achieving Near-Real-Time CDN Delivery path serves LL-HLS/LL-DASH for live-edge viewers (0-30s behind live), while the DVR path serves standard 2-6s segments from the same encoded ladder. Most packagers — Unified Origin, Wowza, Nimble — can output both concurrently without double transcoding. We keep the LL manifest capped at 60 seconds and let the DVR manifest handle everything beyond that. If you try to make one manifest do both, you'll end up with either terrible rewind performance or 8+ seconds of latency.
The second gotcha is monetization. If you're running Server-Side Ad Insertion (SSAI) Through CDN: Seamless Monetization Without Ad Blockers, your ad stitcher and your DVR window are fundamentally fighting each other. A viewer who rewinds 15 minutes shouldn't re-trigger the same stitching decision — or worse, get a fresh ad pod stitched into content they already watched. The fix is session-aware manifest manipulation at the edge. We tag the SSAI session ID to the DVR session and cache personalized manifests keyed by viewer_id + dvr_offset for 60-90 seconds, while shared segments stay cached globally. And never personalize the segments themselves if you can avoid it; personalize the manifest pointers. It keeps your cache hit ratio above 92% even with SSAI enabled, versus sub-40% if you cache per-user segments.
When rewind starts throwing 404s or manifest drift, check three things in order. First, clock sync — if your edge PoPs are even 3-5 seconds out of sync with the origin packager's PDT (Program Date Time), viewers near the live edge will request segments that don't exist yet. NTP is non-negotiable. Second, #EXT-X-PLAYLIST-TYPE — if it flips from EVENT to VOD on sloppy packager configs, players will stop updating and think the stream ended. Third, eviction pressure — a 4-hour window at 5 bitrates (1080p, 720p, 480p, 360p, 160p) is roughly 6-8 GB per channel per hour, so 24-32 GB for 4 hours. Multiply by 100 channels and you're at 2.4-3.2 TB of hot cache. If your edge SSDs are only 1.8 TB, the LRU will start evicting the tail of the window. We alert when DVR segment 404s exceed 0.5% — that's your early warning that your window is too big for your cache tier.
Frequently Asked Questions
How long can an Edge DVR window realistically be without wrecking cache efficiency?
For most live sports and news, 2-4 hours is the sweet spot. Up to 6 hours is doable if you've got SSD-heavy edges and only 3-4 bitrates. Beyond 8-12 hours, you're not doing Edge DVR anymore — you're doing Catch-Up TV and should be writing segments to object storage (S3/GCS) and serving them as VOD assets. We've pushed a 12-hour window for a major broadcaster and saw cache hit ratios collapse to 62% because the working set exceeded the edge's 3TB NVMe allocation.
Does enabling DVR add latency to my live stream?
Not if you isolate the paths. The live-edge manifest should remain a tiny 3-6 segment sliding window with 1-2 second cache TTL. DVR manifests are generated on-request with a ?DVR&start=...&end=... query or via time-shift headers, so they don't contaminate the live path. Where folks add 5-8 seconds of latency is when they force the player to use the DVR-style manifest for everyone. Keep them separate.
Can viewers rewind during SSAI ad breaks?
Yes, but you need to decide the policy. Most broadcasters we work with make ad breaks non-skippable even on rewind — the SSAI stitched manifest for that time range is re-served with the same ads, enforced by the player's timeline. If you let viewers skip ads on rewind, you'll violate most ad contracts. The edge should return the stitched ad segments for that PDT window, not the clean content. Set EXT-X-DATERANGE ad markers as immutable for the DVR window.
Why do players stutter or freeze when scrubbing far back in the DVR window?
Almost always a keyframe/GOP issue, not CDN. If your encoder uses 4-6 second GOPs and a viewer seeks to a non-IDR frame, the player has to download and decode forward from the previous keyframe, which can be 2-3 segments away. Make sure your transcoder is aligned: 2-second GOPs with closed GOPs and IDR frames at every segment boundary. That gives you frame-accurate seeking and no more than 150-250ms of extra decode time on a scrub.