# Revocation and consistency When someone loses access to a document, how soon does needtoknow stop returning it? The delay is the sum of what each layer adds. This page lists the layers, then two ways to configure them: a stated bound on staleness, which suits most workloads, and strict revocation, for access that must stop as soon as it is withdrawn. ## Where the delay comes from 1. **Where the permission lives.** With permissions on the chunks (`MetadataACLAuthorizer`, `TenantAuthorizer`, or an OPA or Cerbos policy that reads chunk metadata), a change takes effect when the chunks are updated: `set_metadata`, a re-index, or the next run of a permission sync such as [Google Drive](integrations/google-drive.md). With a permission service (OpenFGA, SpiceDB, OPA data, Cerbos policies), it takes effect when the service has the change. 2. **The permission service's own consistency.** - OpenFGA with `consistency="MINIMIZE_LATENCY"` (its default) may answer from its cache, when one is configured; `"HIGHER_CONSISTENCY"` skips it. See [OpenFGA](integrations/openfga.md). - SpiceDB with `{"minimizeLatency": True}` (the default here) answers from a recent snapshot, up to the datastore's quantization interval (5 seconds by default); `{"fullyConsistent": True}`, or `{"atLeastAsFresh": {"token": zedtoken}}` with the token from your write, answers from current data. - OPA and Cerbos answer from the policies and data they have loaded; bundle polling or a policy reload adds its interval. 3. **`CachingAuthorizer`.** A cached decision is used for at most `ttl` seconds. `invalidate()` drops decisions at once in its own process, including answers that were still being fetched, and reaches other replicas sharing a Redis within `local_ttl`. See [permission caching](operations.md#permission-caching). 4. **The store's visibility delay**, when permissions live on chunks: some stores show an update to searches only after a refresh (Elasticsearch and OpenSearch refresh every second by default). 5. **Requests already running.** A retrieval that started before the revocation may finish with the decision it already had. needtoknow never caches such an answer after `invalidate()`, but it cannot recall a result already returned, or what a model has already been given. ## Bounded staleness The usual choice: answers are cached, and the longest a revoked permission keeps granting access is known and chosen. ```python authorizer = CachingAuthorizer( OpenFGAAuthorizer(FGA_URL, STORE_ID), ttl=30, shared=RedisCache(redis_client, signing_key=CACHE_SIGNING_KEY), local_ttl=1, ) ``` - **Revocations nobody reports** (a change made directly in the permission service): the delay is at most the service's own staleness plus `ttl`. - **Revocations you report** by calling `authorizer.invalidate(user_id)` after the change: no retrieval that starts in that process afterwards uses the old decision, and other replicas stop within `local_ttl`. The tests revoke a permission under concurrent retrievals, with one and three replicas, and check exactly this. Choose `ttl` as a security decision. 30 seconds removes most repeated calls within a conversation; a few seconds still removes many. ## Strict revocation For access that must stop at once, such as when an account is disabled, make every layer current and put the revocation in the request path of the change itself: - **Ask the service for current answers:** `OpenFGAAuthorizer(..., consistency="HIGHER_CONSISTENCY")`, or `SpiceDBAuthorizer(..., consistency={"fullyConsistent": True})`, or pass SpiceDB the zedtoken of your write with `atLeastAsFresh`. - **Do not cache, or invalidate before you report success.** Either leave `CachingAuthorizer` out, or call `invalidate(user_id)` in the same code that applies the revocation, and only then tell the administrator it is done. `invalidate()` raises if it cannot reach Redis. - **For permissions on chunks,** call `set_metadata` in that same code, and allow for the store's visibility delay. - **Keep `recheck=True`** (the default), so every pre-filtered hit is decided by the authorizer as well as by the filter. The price is a call to the permission service for every retrieval, with its latency and load; the [benchmarks](benchmarks.md) show what an OPA call and a cache cost under load. Even so, a retrieval already running when the revocation happens can complete with its earlier answer: strict means no request that starts afterwards sees the document, not that one in flight is recalled. ## Checking it `RetrievalEvent.started_at` and the returned chunk ids, in your audit log, show whether a revoked document was returned after a given time. To measure your own deployment's delay, revoke a test permission while traffic runs and find the last retrieval that returned the document, as `tests/test_revocation.py` does.