Reducing Latency with SpiceDB Watch Streams for Local Permission Caching
Show how to subscribe to SpiceDB's watch stream to keep an in‑process cache of permission decisions, lowering latency while staying eventually consistent.
25 Mar 2026, 14:17 UTC

When a service needs to answer "can this user act on this resource?" many teams reach for SpiceDB’s Check API. The call is accurate but adds a network round‑trip to a separate authorization service, which can become noticeable under high request rates. The useful takeaway is to keep a small, eventually‑consistent cache of permission decisions locally and update it by subscribing to SpiceDB’s watch stream, so most checks are served from memory while still reflecting changes made elsewhere.
Problem: network latency adds to every permission check
Each Check request incurs the latency of the SpiceDB gRPC/HTTP hop, plus any internal graph traversal. In a latency‑sensitive path (e.g., API gateway or middleware) that overhead can dominate the request budget, especially when the same permission is checked repeatedly for the same subject‑object pair.
Solution: materialize permission tuples via SpiceDB watch
SpiceDB exposes a watch API that streams revisions of relationship tuples matching a filter. By opening a watch for the tuples your application cares about, you can maintain an in‑process map that mirrors the relevant slice of the permission graph. When a check arrives, you first look in the local map; only on a miss do you fall back to a remote Check.
Schema and tuple writes
Define a simple document schema with explicit roles and a computed permission:
definition user {}
definition document {
relation owner @user
relation editor @user
relation viewer @user
permission can_edit = editor + owner
}
Application code writes tuples via the Writes API (omitted for brevity). For example, to grant alice viewer access on document 123:
curl -X POST $SPICEDB_URL/v1/write \
-H "Authorization: Bearer $SPICEDB_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"writes":[{
"operation":"OPERATION_CREATE",
"tuple_key":{
"object":{"object_type":"document","object_id":"123"},
"relation":"viewer",
"subject":{"object_type":"user","object_id":"alice"}
}
}]
}'
Subscribing to the watch stream
Open a watch for all viewer tuples on document 123. The HTTP endpoint returns a stream of JSON objects, each containing a revision and the tuple changes.
# Replace $SPICEDB_URL and $SPICEDB_TOKEN with your endpoint and a token that has read access.
curl -N $SPICEDB_URL/v1/watch?filter.object_type=document&filter.object_id=123&filter.relation=viewer \
-H "Authorization: Bearer $SPICEDB_TOKEN" \
-H "Accept: application/json" | while read -r line; do
# Each line is a JSON watch event.
echo "Received watch event: $line"
# Update local cache (pseudo‑code).
# cache.set(tuple_key, event.revision)
done
The loop keeps a map such as cache["document:123#viewer"]["user:alice"] = revision. When a Check for can_edit is needed, the service first checks whether the subject has any of the underlying relations (editor or owner) in the cache; if all needed tuples are present and their revisions are still valid, the decision is made locally. Otherwise, it calls the Check API and updates the cache with the result.
Trade‑off: eventual consistency and operational complexity
Using a watch‑driven cache introduces a bounded staleness window: a tuple change is only visible after the watch event is processed. For most UI‑driven permissions this delay is acceptable, but for strict security‑critical checks you must either tolerate the window or bypass the cache. Additionally, running a watch client adds operational concerns: connection management, back‑off on failures, and metric collection for cache hit/miss ratios.
Closing: put it into practice
Start by identifying a bounded context (e.g., document collaboration) where the same permission is checked frequently. Define the schema, write the initial tuples, and deploy a watch client that populates a simple in‑process map. Instrument the client to log cache hits and remote fallbacks; verify that the hit rate rises over time while the latency of the permission check drops. Adjust the watch filter to include only the tuples you actually need, keeping the cache size manageable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.