Concepts · Caching
Snapshots, ETags and failing static
The database is never on the evaluation path. Every change becomes a versioned snapshot that is pushed through Redis to memory, and when a dependency breaks we keep serving the last good one.
The ruleset snapshot#
Each environment has exactly one snapshot: the complete, self-contained ruleset for that environment. It holds every flag with its variations, targeting rules and per-flag salt, the resolved definitions of the segments those rules reference, and any JSON Schemas. It does not reference anything else, so a server SDK needs one request to evaluate every flag.
The snapshot's content hash is its version, and the same value is the HTTP ETag. Two snapshots with the same hash are identical, which is what makes revalidation cheap.
A snapshot is always rebuildable from the control-plane database. Losing Redis loses speed, never data.
Snapshot propagation#
When you save a flag, toggle an environment, edit a segment or change a key's state, the API regenerates that environment's snapshot in the same operation as the committed change and writes it to Redis (write-through). It then publishes an invalidation on Redis pub/sub; every eval instance reloads the snapshot from Redis into its in-memory (L1) copy. A periodic comparison of version hashes catches any missed message.
- Commit and snapshotAPI commits the change and regenerates the environment snapshot, computing its hash.api
- Write-through to RedisThe snapshot is stored in the same operation as the committed change. Reads never fill Redis from Postgres on a miss.redis
- InvalidateA pub/sub message tells everypub/sub
evalinstance to reload; a periodic hash comparison guards against a lost message. - SDK observesA polling SDK sendssdk
If-None-Matchand receives the new body on its next poll; a streaming SDK is told immediately.
Server SDKs (JavaScript and Python) stream by default (GET /v1/sdk/stream; any event triggers an ETag refetch of the snapshot), which is what keeps the console-to-SDK p95 under 5 s. While the stream is down they poll every 30 seconds (jittered by ±20%) and reconnect; while it is up they still poll every 5 minutes as a safety net. With streaming: false an SDK sees a change within about one poll interval, and every poll, including a 304, counts as a request against your plan. The server can tell SDKs to slow down with Retry-After.
ETag revalidation#
A server SDK stores the version it holds and sends it back on every poll. If nothing changed, the response is a tiny 304 with no body:
GET /v1/sdk/snapshot HTTP/1.1
Host: sdk.darkship.tech
Authorization: Bearer ds_srv_prod_9Qm...
If-None-Match: "492be076feb012e6aa08cf33ebb022944ef343728b997fa343cad22f29ad8712"
HTTP/1.1 304 Not Modified
ETag: "492be076feb012e6aa08cf33ebb022944ef343728b997fa343cad22f29ad8712"
Cache-Control: private, max-age=0, must-revalidateAfter a change the same request returns 200 with the new snapshot and a new ETag. The exact Cache-Control values are placeholders pending load tests; the contract (ETag and conditional requests) is stable, and a CDN could later sit in front of it without changing SDKs.
Fail static#
If Redis or PostgreSQL becomes unavailable, eval keeps serving the last snapshot it holds in memory. It does not return errors, and it does not fall back to querying Postgres. Staleness is bounded only by how long the dependency is down, and is surfaced in metrics and in the /ready health output.
Try it. Break something and see what stays up.
Fail-static simulator#
All dependencies upConsole writes publish a new snapshot; evaluations resolve from memory. Nothing is degraded.
Dependency outage matrix#
| Down | Evaluations | Flag changes | Visible as |
|---|---|---|---|
| Redis | Served from L1 memory; correct as of the last snapshot | Saved but not propagated until Redis returns | Snapshot age rising on /ready |
| PostgreSQL | Unaffected | Rejected; console shows a banner | Control-plane errors only |
| Both | Served from L1 memory | Not possible | Snapshot age rising; banner in console |
eval itself | Server SDKs keep their in-memory ruleset; client SDKs use session cache | Unaffected | SDK error events; status page |
Cold start edge cases#
- A fresh
evalinstance starts while Redis is down. It has no snapshot, so it reports not-ready and receives no traffic. Other instances keep serving. - A fresh server SDK starts while we are unreachable. With no ruleset it returns your defaults (reason
NOT_READY). Supply abootstrapsnapshot you saved earlier to avoid even that. - A running server SDK loses us. It keeps evaluating from its last ruleset indefinitely, retrying with exponential backoff and jitter, and emits an
errorevent.
During an outage, a flag you flipped minutes ago may not be flipped for running SDKs. We choose "stale but consistent with a real past state" over errors. If you need an emergency off-switch that doesn't depend on us, wrap the risky path with your own config as well.
Quota never stops evaluation#
The same philosophy applies to billing and quota: evaluation endpoints keep serving in every plan state, including over-limit. Only control-plane writes that create new things (flags, keys, members) are restricted after a grace period.
Thanks. This helps us prioritise the docs.