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.Pathfor 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— serializedinfo.jsonbodies. Key:auth.InfoKey(r.Host, r.URL.Path, tier)—r.Host + r.URL.Pathfor public, with the same|v1|<tier>suffix on gated identifiers. Host is included so the embeddedidfield 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 viasource.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.
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.
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
filesystemalone —flocksemantics 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 bothbackend: redisand acoordinator:block is a config error. - Each replica needs the same
path:and the samecoordinator.redissettings to participate in the shared index. The Redis namespace is derived from the absolutepath:, 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. Statsreports from the Redis index — cheap and consistent across replicas (no directory walk).- The
filesystembackend's local-modeflockfile 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. anaws s3 rm --recursiveof the slot prefix), or let object lifecycle expiry roll them over.redis—DELthe render/info key range (orFLUSHDBa 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=86400ETag(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.