Skip to content

Caching & delivery

Resolving a poster frame from a provider is the expensive, fragile step, so Vumbnail does it as rarely as possible. Every thumbnail is resolved once and then served from progressively cheaper caches.

On each request the Worker checks, in order:

  1. Cloudflare edge cache (caches.default). A warm hit returns immediately with no further work. Clean and watermarked variants are cached under separate keys (the watermarked key carries an internal ?_wm=1 marker that is hidden from clients).
  2. R2 persistent store (vumbnail-thumbnails bucket). The resolved variant is looked up at thumbnails/<videoId>/clean.jpg or thumbnails/<videoId>/wm.jpg. If the watermarked variant is missing but the clean one exists, the watermark is applied from the stored clean image rather than re-resolving the source.
  3. Source resolution. Only on a full miss does the Worker fetch the poster from the provider (see below), store the clean variant to R2, derive the watermark if needed, populate the edge cache, and return the image.

A successfully resolved Vimeo source is memoized to R2 so repeat misses reuse the discovered image URL instead of re-walking the provider’s metadata.

  • Browser cache: ~1 day for both variants.
  • Edge/CDN cache: up to ~30 days for the watermarked variant (it changes rarely and is the most cost-sensitive).
  • Fallback graphics (error.jpg / locked.jpg): served no-cache, must-revalidate so a temporary failure doesn’t pin a bad image.

Inputs: the parsed ThumbnailRequest, the resolved variant (clean vs. watermarked, from the freemium decision), and a debug flag that disables cache reads/writes.

Outputs: the image bytes plus the variant-appropriate cache headers, and — on a sampled fraction of requests — internal attribution counters describing which layer answered.

  • Negative cache. A failed resolution is recorded in R2 under thumbnails/_negative/<cacheId>.json with a state and a backoff-based expiry, so a deleted, blocked, or rate-limited video isn’t re-fetched on every hit. States: not-found, password-protected, blocked, rate-limited, unknown. Backoff roughly doubles the retry window up to a cap.
  • Shared fallbacks. error.jpg and locked.jpg are single shared R2 objects reused across all failing IDs, so failures cost almost nothing to store.
  • Watermark transform failure — fails open: the clean image is served and the failure is logged as a structured signal rather than thrown.
  • Debug mode (?debug, ?_debug, or a watermark-test. host) — bypasses edge cache reads and writes and returns no-store, so probes always exercise the live path.
  • Cache key fragmentation is a known risk: the clean/watermarked split and query handling must not accidentally create multiple keys for one image. Miss- path attribution (below) exists to catch this.

R2 layout and the cached records:

// R2 object keys under the `thumbnails/` prefix
// <videoId>/clean.jpg clean variant
// <videoId>/wm.jpg watermarked variant
// _shared/error.jpg shared failure graphic
// _shared/locked.jpg shared password graphic
// _negative/<cacheId>.json negative cache record
// _resolver-success/<id:pw>.json memoized Vimeo resolution
type NegativeCacheRecord = {
state: "not-found" | "password-protected" | "blocked" | "rate-limited" | "unknown"
reason: string // e.g. "source-http-404", "vimeo-block-detected"
backoffLevel: number // drives the retry window (≈ 2^min(level,3))
softExpiresAt: string // ISO; serve-stale-while-revalidate boundary
hardExpiresAt: string // ISO; record is discarded after this
}
type ResolverMemo = {
url: string // discovered upstream image URL
resolvedAt: string
softExpiresAt: string
hardExpiresAt: string
}

Cache-attribution counters sampled on ~1% of requests (configurable via THUMBNAIL_INTERNAL_ATTRIBUTION_SAMPLE_RATE) include cdnMatchHit/Miss, r2VariantHit/Miss, r2CleanForWmHit/Miss, r2NegativeHit/Miss/Stale, and r2SharedFallbackHit/Put.

  • Miss-path ranking (P1). Stage-1 attribution is deployed but a long-enough sample to rank edge-cache misses vs. R2 misses vs. negative-cache misses hasn’t been collected. The optimization (cache-key fragmentation fix, removing redundant wm → clean recursion, or reordering negative-cache reads) is gated on that data.
  • Miss coalescing via Workers Cache. Cloudflare’s new pre-Worker Workers Cache provides request collapsing (one Worker run per key per colo, free on any plan), which is the single-flight this layer otherwise lacks. It keys on URL not referrer, so it is not safe to front the clean/watermark variant path until the variant is encoded in the URL (see Edge watermark router). Direction: pilot on fixed-variant surfaces (shared fallback graphics, a test route) first; adopt on the image path only after the Snippet URL-encodes the variant, then it can replace the hand-rolled caches.default layer. The savings estimate finds this is not a cost play (~$1–$12/mo, ceiling ~$13/mo, negative if naive), so sequence it for collapsing/latency — not savings.
  • Projection/cache TTL tuning for the entitlement allow-list interacts with freshness — see Paid access and the global Open questions.