Skip to content

Caches

Three cache slots × four storage backends, plus none to switch a slot off. Each slot can run a different backend.

heap filesystem s3 redis
RenderCache ✓ size-bounded LRU ✓ size-bounded LRU + startup walk ✓ lifecycle-policy eviction ✓ TTL + MAXMEMORY eviction
InfoCache
OriginCache

backend: none is valid for any slot and disables it (see none).

Slots

  • RenderCache — rendered IIIF image responses. Key: auth.RenderKey(r.URL.Path, tier)r.URL.Path for public identifiers, r.URL.Path + "|v1|<tier>" for gated ones (e.g. /iiif/3/sample.jpg/full/max/0/default.jpg). See auth-tier keying.
  • InfoCache — serialized info.json bodies. Key: auth.InfoKey(r.Host, r.URL.Path, tier)r.Host + r.URL.Path for public, with the same |v1|<tier> suffix on gated identifiers. Host is included so the embedded id field can't leak across hostnames.
  • OriginCache — fetched source bytes. Key: <source-name>:<identifier> (e.g. s3:photos/cat.tif). Applied automatically to HTTP and S3 sources via source.Cached; filesystem bypasses (already local). Not auth-tiered — origin bytes are identical regardless of who authorized the request.

Backends

Each slot takes one of the five backends below, constructed via the cache.BuildRender / BuildInfo / BuildOrigin factories (cmd/iiirisd/main.go); bad config fails fast at boot.

none

Disables the slot. Every lookup misses and every store is discarded, so the server always does the full work the slot would have cached.

caches:
  render:
    backend: none

Use it to measure or reason about the pipeline without caching effects — the comparative benchmark harness (tools/bench-compare) sets all three slots to none for its "caches off" pass. No max_bytes, path, or other fields apply.

heap (default)

In-memory map[string]*item with size-bounded LRU eviction.

caches:
  render:
    backend: heap
    max_bytes: 268435456   # 256 MiB; 0 = unbounded

Stats and PurgeAll are supported (visible in the admin UI).

filesystem

Sharded on-disk cache: <root>/<aa>/<bb>/<full-sha-hex>. The filesystem itself is the index — no separate index file or in-memory map. Get is os.Open plus a best-effort mtime touch (LRU bookkeeping); Put is tmp-file + os.Rename. Both are lock-free.

caches:
  render:
    backend: filesystem
    path: /var/cache/iiiris/render
    max_bytes: 5368709120   # 5 GiB; 0 = unbounded

If max_bytes is reduced after iiirisd has been running with a larger budget, the next startup trims down to the new budget.

Concurrent processes on one host are safe. Multiple iiirisd processes may share the same path: — eviction is serialized across processes by a flock-protected critical section (<path>/.lock). Cap enforcement is best-effort across processes: between eviction passes the on-disk total may briefly exceed max_bytes by up to write_rate × walk_interval (typically <1% of cap in practice).

Local mode is single-host. Multiple iiirisd replicas on different hosts pointed at a shared NFS/EFS mount are not supported by filesystem alone — flock semantics over NFS are unreliable and the walk-based index will silently diverge. For multi-replica shared-FS deployments configure a Redis coordinator (see Distributed mode below).

Distributed mode (Redis coordinator)

To run multiple iiirisd replicas against a shared filesystem mount (EFS, NFS, GlusterFS), promote the filesystem backend to distributed mode by adding a coordinator: block. The files stay on disk; the index (entries → sizes, LRU order, total bytes) moves to Redis where it stays consistent across replicas.

caches:
  render:
    backend: filesystem
    path: /mnt/efs/iiiris/render
    max_bytes: 10737418240        # 10 GiB
    coordinator:
      redis:
        addr: redis.svc:6379
        password_env: REDIS_PASSWORD
        prefix: iiiris            # optional namespace

Notes:

  • The coordinator's Redis is separate from backend: redis (the redis-as-backend mode). Setting both backend: redis and a coordinator: block is a config error.
  • Each replica needs the same path: and the same coordinator.redis settings to participate in the shared index. The Redis namespace is derived from the absolute path:, so different paths give different index keyspaces (e.g. one Redis can host render + info + origin coordinators side by side).
  • On startup each replica runs a reconciliation pass under a lease: orphan files (in tree, missing from index) are re-indexed; ghost entries (in index, missing from disk) are dropped. This is a no-op in steady state.
  • Eviction is serialized by a TTL'd Redis lease (SET NX EX, 30s). Only the lease holder evicts. If the holder dies, the lease expires and another replica takes over.
  • Stats reports from the Redis index — cheap and consistent across replicas (no directory walk).
  • The filesystem backend's local-mode flock file is unused in distributed mode; Redis is the only coordinator.

Cap counts only hash-named files

max_bytes is enforced against files matching the <64-char-lowercase-hex> sha shape. Operator-dropped files (READMEs, .DS_Store, the cache's own .lock and .iiiris-cache-version sentinels) do not count toward the cap and are never evicted. If you drop a 5 GB tarball into the cache root, the on-disk footprint exceeds max_bytes by 5 GB and iiirisd will not touch the tarball.

Migration on upgrade

The first time iiirisd attaches to a directory that contains pre-FS_CACHE2 cache files (no .iiiris-cache-version sentinel present), the legacy hash files are removed and the layout is initialized fresh. Operator-dropped files are preserved. A WARN log line records the wipe. Subsequent restarts find the sentinel and are no-ops.

macOS dev performance note

The filesystem cache is functionally correct on macOS APFS but materially slower than Linux ext4 — directory walks are ~5× slower and concurrent-Put p99 is ~30× slower. macOS is a development target only; production numbers apply on Linux.

Stats and PurgeAll are supported.

Windows path-length

On Windows the legacy MAX_PATH ceiling is 260 characters. The sharded layout adds about 70 characters to whatever path: you configure (<root>\aa\bb\<64-char-hash>), so the cache itself is fine; the risk is the root being deep already. Anchor path: near the volume root (for example C:\iiiris\cache\render, ~25 characters) and the ceiling is not in play. Long-path support (\\?\ prefix or the LongPathsEnabled policy) is not required.

s3

S3 (or S3-compatible) object store.

caches:
  render:
    backend: s3
    max_bytes: 0                       # MUST be 0 — see below
    s3:
      bucket: iiiris-cache
      region: us-east-1
      prefix: render
      endpoint: ""                     # optional

Eviction is the operator's responsibility — set an S3 lifecycle policy on the bucket (by age, by tag, etc.). Setting max_bytes != 0 with backend: s3 errors out at boot with a clear message.

Stats and PurgeAll are not supported (would require listing the bucket / distributed deletion); the admin UI shows "—" and "not supported by backend" respectively.

Uploads stream via the AWS S3 Manager's Uploader (5 MiB parts, 5-way concurrency by default). The cache body never has to fit in memory at once; above the part-size threshold the upload silently switches to multipart and the manager handles abort-on-error cleanup.

redis

Redis-backed object store. Suitable for multi-replica iiirisd deployments that need a shared cache — Redis's atomic single-key semantics mean concurrent Put from different replicas can't tear a value.

caches:
  render:
    backend: redis
    max_bytes: 0                       # MUST be 0 — see below
    ttl: 1h                            # optional; per-entry TTL via SET EX
    redis:
      addr: redis.internal:6379
      password_env: REDIS_PASSWORD     # env-var-resolved (preferred over inline password)
      db: 0
      tls: true
      prefix: iiiris                   # namespace for shared-Redis deployments

Eviction is the operator's responsibility — set Redis MAXMEMORY plus an eviction policy (allkeys-lru is the typical pick). Optionally configure ttl: to attach a per-entry TTL so keys expire on their own clock, independent of memory pressure. Setting max_bytes != 0 with backend: redis errors at boot — iiiris doesn't do client-side eviction against Redis.

The Redis backend stores binary-safe values: the cache value bytes include a 2-byte big-endian length prefix for the content-type, followed by the content-type string, followed by the body. Binary image bytes round-trip exactly (no base64 + JSON overhead).

Stats reports entry count via SCAN of the iiiris prefix. Byte total is not tracked (Redis doesn't expose it cheaply); the admin UI shows . PurgeAll walks the prefix with SCAN + DEL in batches so it's cooperative with other clients on the same Redis.

Auth-tier keying

When the IIIF Authorization Flow 2.0 subsystem is configured, RenderCache and InfoCache keys are salted with an auth tier (public / full / substitute) derived from the access decision, so a denied request never serves bytes cached for an authorized user (or vice versa). The public tier's key is byte-identical to pre-auth iiiris, so adding auth doesn't churn unrelated entries. Full tier vocabulary, key shapes, and the v1 versioning: iiif-auth.md § Cache-tier keying.

OriginCache is not auth-tiered — the bytes fetched from a source are identical regardless of who's authorized, so tiering them would duplicate cache space for no benefit.

Flushing on a behaviour change

Cache keys are derived from the request URL (plus auth tier), not from the pipeline's output settings. So a config change that alters the bytes or dimensions produced for a given URL — most notably switching image.metadata between strip and preserve — does not invalidate existing RenderCache / InfoCache entries. Stale pre-change results keep serving until they age out.

When you change such a setting (including upgrading to the release that made image.metadata: strip the default), flush the render and info caches:

  • heap — restart the process (heap caches are in-memory, so a restart clears them).
  • filesystem — delete the render/info cache directories (or their contents) while stopped, then restart.
  • s3 — empty the render/info key prefixes (e.g. an aws s3 rm --recursive of the slot prefix), or let object lifecycle expiry roll them over.
  • redisDEL the render/info key range (or FLUSHDB a dedicated cache DB), or let TTL/MAXMEMORY eviction roll them over.

OriginCache holds raw source bytes and is unaffected — it never needs flushing for a metadata-policy change.

HTTP cache semantics

Independent of the cache backend, every IIIF response includes:

  • Cache-Control: public, max-age=86400
  • ETag (sha256 of body, first 16 hex chars)
  • Last-Modified

Conditional GETs (If-None-Match / If-Modified-Since) are honored via http.ServeContent and return 304.