Choosing a SpiceDB Consistency Mode: ZedTokens, Stale Reads, and When fully_consistent Is Worth It
How to pick a SpiceDB CheckPermission consistency mode: default to minimize_latency, persist ZedTokens for read-after-write flows, and reserve fully_consistent for rare high-stakes decisions.
14 Feb 2026, 13:59 UTC

The problem: correct decisions without paying full-consistency latency everywhere
SpiceDB evaluates CheckPermission against a snapshot of your relationship data. On a replicated datastore, a check that runs immediately after a write can hit a replica that has not seen the write yet, and you get a deny for a permission you just granted. The naive fix — marking every check fully_consistent — forces each evaluation to read at the latest revision, which adds latency and load on every request, including the 99% that never race a write.
The useful takeaway: use minimize_latency as your default, capture the ZedToken returned by writes and checks, and pass that token back with at_least_as_fresh only on the follow-up requests that depend on a recent write. Reserve fully_consistent for rare, high-stakes decisions. This gives you read-after-write correctness where it matters and cheap checks everywhere else.
Prerequisites
- A reachable SpiceDB deployment. A local container is fine for development; test consistency behavior on the same datastore backend (Postgres, CockroachDB, Spanner, or in-memory) you run in production, because replication and revision garbage collection differ per backend.
- A written schema and some relationships loaded.
- A client built from the official Authzed API definitions, or the
zedCLI. - Knowledge of which application flows write a relationship and then immediately depend on it (inviting a member, sharing a document, granting a role).
Step 1: Default hot paths to minimize_latency
Every CheckPermission request carries a consistency block. For routine authorization on reads — rendering a page, listing a resource — use minimize_latency. SpiceDB picks a cached or replica snapshot and answers fast. The risk is a bounded window of staleness, which is acceptable when no related write just happened in this user's session.
// gRPC CheckPermissionRequest (field names per the Authzed API for your pinned version)
{
"consistency": { "minimizeLatency": true },
"resource": { "objectType": "document", "objectId": "doc-123" },
"permission": "view",
"subject": { "object": { "objectType": "user", "objectId": "u-42" } }
}Every response — from both writes and checks — includes a ZedToken: an opaque string identifying the revision the answer was computed at. Treat it as opaque. Never parse, compare, or fabricate tokens client-side.
Step 2: Persist ZedTokens on write-then-check flows
When a user action writes a relationship and the next request needs a decision that depends on it, store the token returned by WriteRelationships and pass it back with at_least_as_fresh. This tells SpiceDB: evaluate at a revision at least as new as this token, so the check cannot miss the write.
// After WriteRelationships returns writeResp:
saveToken(userID, resourceID, writeResp.writtenAt.token)
// On the follow-up check:
{
"consistency": {
"atLeastAsFresh": { "token": loadToken(userID, resourceID) }
},
"resource": { "objectType": "document", "objectId": "doc-123" },
"permission": "edit",
"subject": { "object": { "objectType": "user", "objectId": "u-42" } }
}Where to store tokens is a real tradeoff:
| Granularity | Freshness | Cost |
|---|---|---|
| Per-resource (column on the resource row) | Exact for that resource | More storage and plumbing |
| Per-user or per-session | Covers everything the user touched | Simple; can force fresher reads than needed for unrelated resources |
A common pattern is a single per-session token that you overwrite with the newest token seen — since a newer revision covers older writes, keeping the latest token is sufficient for that session's read-after-write guarantees.
Step 3: Reserve fully_consistent for rare, high-stakes decisions
Use fully_consistent where a wrong answer is expensive and the call is infrequent: admin privilege grants, destructive actions like deleting an organization, or access to highly sensitive records. Blanket fully-consistent checks are usually the wrong default because they serialize your authorization path against the datastore's latest revision on every request, erasing the caching and replica-read benefits that make SpiceDB fast at scale.
Sidebar: filtering lists
If you need "all documents this user can view," do not loop CheckPermission over candidate IDs. Use LookupResources, which streams the permitted resource IDs and accepts the same consistency controls — so your stored ZedToken works there too.
Expected checks before you ship
- Read-after-write test: in an integration test, write a relationship, immediately check with the returned token via
at_least_as_fresh, and assert the expected allow. Run it against your production datastore backend, not just in-memory. - Staleness reproduction: write a relationship, then check with
minimize_latencyand no token. On a replicated backend you should be able to observe a stale deny at least occasionally; on a single-node local container you may not, which is exactly why backend parity matters. - Latency comparison: benchmark
minimize_latencyversusfully_consistenton a representative workload and inspect SpiceDB's server metrics. Publish only numbers you measured yourself; do not quote figures from other people's benchmarks. - Token expiry handling: old revisions are eventually garbage collected. Verify what error your pinned SpiceDB version returns for an expired token and that your client handles it.
Recovery when a decision looks wrong
- Suspected stale read: re-run the same check with
fully_consistent. If the answer flips, the original was stale — confirm the write path is storing and passing tokens correctly rather than upgrading every check. - Expired ZedToken error: the stored token references a garbage-collected revision. Do not fail the request hard; retry the check with
minimize_latency(acceptable for old sessions, since the write is long past any replication window) orfully_consistentfor sensitive paths, and replace the stored token with the one from the new response.
Limitations
Field names, error types, and token expiry behavior are version-sensitive — confirm them against the API reference and the consistency/ZedToken documentation for the exact SpiceDB release you run. Consistency semantics and revision retention differ by datastore backend, so conclusions drawn from a local in-memory instance do not transfer automatically to CockroachDB or Spanner in production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.