Archive cache on an edge node¶
A node that serves someone else's archive rather than its own — over remotes or a legacy source — goes to that source for every single fragment. The archive cache changes this: a fragment that has been read settles on a local cache disk, and the next request for the same part of the archive is served locally, with no network round trip.
That is how a CDN edge is built: the origin keeps the archive, the edges keep whatever the viewers actually watch.
The cache is transparent. It changes where the bytes come from, not what is served: fragment URLs, playlists and archive boundaries stay exactly the same for the viewer.
What makes a cache disk different from an archive disk¶
A cache disk is a disk role, not a flag on a disk:
- live recording never picks it — the archive is written only to disks listed in
disks; - stream retention does not govern it — a stream's
max_depthandmax_bytesdo not apply to the cache; - space is freed by eviction — by how long ago a blob was last accessed, not by how old its content is;
- it is isolated in the catalog — cache blobs live in their own tables, and archive traversals (cleanup, backfill, the split of used space into buckets) do not see them at all.
That is why an edge's cache does not show up on the dashboard as an ownerless archive, and why cache eviction does not compete with archive recording for the same mutex.
Configuration¶
The cache is configured in two places: the disks on the node, the switch on the stream.
dvr:
root: /storage
cache_disks:
- path: ssd1 # relative to root, same as an archive disk
max_bytes: 536870912000 # cap on the cache on this disk, bytes
min_free_bytes: 10737418240
- path: ssd2
streams:
- name: cam1
remotes:
- url: http://origin:5080
cache: {} # turns caching of archive reads on
Cache disk parameters:
| Parameter | Description |
|---|---|
path |
disk path relative to root |
max_bytes |
how much the cache may occupy on this disk; empty — bounded only by free space |
min_free_bytes |
how much free space to leave on the volume |
The limits are independent: eviction runs until the disk fits within both. A cache disk has no mode — Degraded describes a member of the archive RAID, and the cache replicates nothing.
The stream's cache section is a top-level section of the document, a sibling of dvr, not a field inside it: caching and recording are independent properties. An edge caches someone else's archive while recording none of its own. The section has no fields yet — its mere presence enables caching.
Config validation rejects:
- a path listed both in
disksand incache_disks; - a duplicate within
cache_disks; - a cache disk whose path equals
root— eviction would wipe the catalog's home.
Different paths on the same physical volume are a valid configuration, but a warning is logged at startup: eviction frees space that the archive immediately takes back.
A cache section on a node that has no cache disks is a valid no-op with a warning in the log. That is normal in a cluster, where one stream document spreads across different nodes.
A pure edge¶
Storage made of root and cache disks only, with no archive disks, is a valid configuration: there is a catalog, there is no live recording, and the archive is pulled in from remotes.
dvr:
root: /storage
cache_disks:
- path: ssd1
max_bytes: 536870912000
streams:
- name: cam1
remotes:
- url: http://origin:5080
cache: {}
Warning
root is the catalog's home, not a write disk. An empty disks list used to mean "write straight into root"; now it is a write error visible in the statistics instead of a silent write past the RAID. If the node is supposed to record an archive, list the disks explicitly in disks.
Cache admission¶
Caching everything on first request means evicting the popular in favour of the random: a single seek deep into the archive would push out what everybody is watching. Admission therefore has two axes, configured on the node for the cache as a whole:
| Parameter | Description |
|---|---|
cache_fresh_age_secs |
a fragment younger than this depth (seconds back from now) is cached on the very first request; one day by default |
cache_admission_hits |
which repeat request starts caching a fragment older than the freshness boundary; the second by default, 1 disables the barrier |
dvr:
root: /storage
cache_fresh_age_secs: 86400
cache_admission_hits: 2
cache_disks:
- path: ssd1
Almost everybody watches the fresh tail of the archive, so it enters the cache straight away. The depth of the archive interests a handful of viewers — it enters the cache only when asked for again.
Read source order¶
An archive fragment is looked up along a chain, and the first source to answer closes the request:
- the live stream's segmenter;
- the cache;
- the local archive;
- the cluster (remotes);
- the legacy
old_m4fsource; - the on-disk legacy archive.
A cache hit answers without touching anything below it. If a fragment is present both on an archive disk and on a cache disk, the read is served by the cache: an SSD cache beats spindles. A stream without a cache section has no cache step in the chain at all.
What a miss puts into the cache¶
A successful read from a source below the cache is written to a cache disk together with its init fragment. The differences from archive recording:
- the
max_depthcutoff does not apply — what the viewer asked for goes into the cache, even if it is deeper than any configured archive depth; - a multi-track segment is stored whole, with every track — so a viewer's bitrate switch is served by hits rather than misses;
- the write is idempotent — several viewers missing on the same fragment at once leave exactly one copy in the cache;
- a cache write error does not affect the client's response — it already has the bytes; the error is only logged and counted in metrics.
Read-ahead¶
Archive viewing is sequential, so after serving a read Sapsan fetches the next fragment of the same track in the background if it is not in the cache yet. Read-ahead works on a pure edge and when reading from a remote source, is limited to one fragment ahead, and never delays the client's response.
This is not warm-up: there is no scheduled or EPG-driven cache filling here.
Eviction¶
Space on a cache disk is freed by whole hourly blobs in order of last access — the least recently used goes first — until the disk is back within both limits.
- Content age is not a criterion: yesterday's popular show outlives today's unpopular one, and the current hour is evicted on equal terms with the rest.
- The single exception is a blob a writer is appending to right now: it stays, and the next least recently used one goes instead.
- Archive cleanup (a stream's
max_depthandmax_bytes) and episode protection do not consider cache disks at all.
The eviction order is kept by its own catalog index and survives a restart. The access mark is flushed to the catalog lazily — at most about once per 10 minutes per blob — so that reading does not turn into writing.
Cache accounting (used volume, access times) is restored at startup by a catalog range scan, without walking the data files. A wiped cache disk is a valid cold state: the server starts, the cache fills from scratch, the archive is untouched.
Timeline and depth on an edge¶
The rewind depth and archive ranges of a cached stream are computed as the union of the local catalog (archive and cache) and every remote source — and announced in rewind playlists and in archive range responses. A viewer on the edge sees the same depth as on the origin.
Source windows are merged as a list, preserving the gaps between them: a solid interval from a source's start to its end must not stand in for that list — otherwise the timeline paints intervals green where there is no data, and a click on them lands in a 404. Gaps no larger than the requested resolution are still merged by the usual rule.
If the origin is unreachable but the requested range is fully cached, the playlist is built from the local catalog and the fragments are served from the cache — playback continues.
Observability¶
Every archive read is attributed to the link of the chain that served it:
- cache — served by the node's cache disk (a hit);
- local — served by the node's archive disk;
- remote — a remote source had to be queried (a miss).
Only a client's fragment read counts as an archive read. Output from the live segmenter and read-ahead fetches are not attributed — otherwise read-ahead would inflate the hit rate the better it worked; fetches are counted separately.
Cumulative counters go to Prometheus and local-prom:
| Metric | Meaning |
|---|---|
dvr_archive_reads_total{source} |
archive reads per cache / local / remote axis |
dvr_archive_read_bytes_total{source} |
bytes of those reads |
dvr_read_ahead_fetches_total |
read-ahead fetches |
dvr_disk_reads_total{disk_id}, dvr_disk_read_bytes_total{disk_id} |
disk accesses; on a cache disk these are its hits |
dvr_disk_cache_bytes{disk_id} |
cache volume on the disk |
The local GET /streamer/api-v4/dvr endpoint reports each cache disk's role, volume and limit, and the node's read rates along the three axes. Cumulative counters are not exposed in the management API: it carries rates and gauges.
The hit rate is never delivered as a ready-made number — it is cache / (cache + local + remote), and the consumer computes it: add the rates up, divide afterwards. An average of averages lies when nodes carry unequal weight.
The console screens that show this are described in the Catena documentation: DVR capacity across the cluster and node settings.
How this differs from lazy replication over remotes¶
Remotes have a similar behaviour: a fragment read from a remote server is appended to the local archive. The difference is fundamental:
| Lazy replication over remotes | Archive cache | |
|---|---|---|
| Written where | to archive disks | to cache disks |
| What for | to move someone else's history here for good | to serve repeat requests locally |
| Freed when | by cleanup according to the stream's retention | by eviction according to last access |
| Archive disks required | yes | no, an edge can run without them |
Replication is about relocating an archive; the cache is about saving network on what is popular.