Skip to content

Cache remote media by origin server and media ID

  • Status: accepted; the cache is made durable, and its lookup, expiry and cap-0 rules are restated, by ADR-0119
  • Deciders: James Chapman
  • Date: 2026-10-05

Context and Problem Statement

Before this change, every request for a remote mxc:// URI re-fetched the media from the origin server and re-ran it through the local admission pipeline (scanner, decoder, quarantine, thumbnailer). This wasted bandwidth and CPU and allowed a remote server to amplify storage/thumbnail work by returning the same media ID repeatedly (security audit finding OUT-4). There was no cache, TTL, or eviction.

Decision Drivers

  • Repeated requests for the same remote URI should serve the already-admitted local copy.
  • The cache must be bounded so it cannot itself become a denial-of-service surface.
  • Cached entries must expire so that remote content updates are eventually visible.
  • Eviction must be predictable (LRU) rather than arbitrary.

Considered Options

  1. Cache only the raw remote bytes before admission. Rejected: the admission pipeline (scanner, decoder, quarantine) is the expensive part; we want to avoid re-running it.
  2. Cache by local media ID only. Rejected: callers arrive with an (origin_server, media_id) pair, not a local ID, so the mapping must be keyed by the remote pair.
  3. Cache by (origin_server, media_id) pointing to the admitted local record, with TTL and LRU eviction. Accepted: it avoids duplicate fetches and admission work, is keyed by what the caller provides, and is bounded.

Decision Outcome

Chosen option: "in-memory vector cache keyed by (origin_server, media_id) that stores the local media ID assigned on first admission, with a configurable TTL and least-recently-used eviction when the entry cap is reached", because it saves both network and admission work while keeping memory bounded.

Positive Consequences

  • Repeated remote media requests do not hit the network or re-admit bytes.
  • The cache size is capped and old/least-used entries are evicted.
  • Cache entries are invalidated when the underlying local record is no longer available (quarantined or removed).

Where the lookup sits

The media service consults the cache (media::find_cached_remote_media) in fetch_remote_media_live, after the remote_fetch_enabled/allow_remote refusal and before the client-outbound budget slot, discovery and the outbound request. A lookup placed after the fetch, inside media::fetch_remote_media, stops duplicate storage but still sends one outbound request per client request, which is the amplification this decision exists to remove; that was the first implementation, and it was wrong. The post-fetch lookup remains only so two requests that both missed the cache do not admit the same media twice.

An entry for a key replaces any older entry for it, an expired entry or one whose record is no longer available is erased on lookup, and remote_media_cache_max_entries = 0 disables the cache rather than removing its bound.

Negative Consequences

  • A zero TTL disables caching; a very long TTL delays visibility of updated remote media. Defaults are chosen for a one-day window.
  • Security audit finding OUT-4 in docs/security-audit-report-2026-09-29.md.
  • Related media repository decisions ADR-0026 and ADR-0068.