Cyclical Relationships and Traversal Limits
SpiceDB answers permissions questions by traversing a tree constructed from your schema (structure) and relationships (data).
When you call CheckPermission, SpiceDB starts at the resource and permission you specified, then walks through relations and permissions until it either finds the subject or determines the subject doesn’t have access.
How Traversal Works
Consider this simple example:
┌─────────────────────┐
│ document:readme │
│ permission: view │
└─────────┬───────────┘
│ viewer relation
▼
┌─────────────────────┐
│ group:engineering │
│ permission: member │
└─────────┬───────────┘
│ member relation
▼
┌─────────────────────┐
│ user:alice │
└─────────────────────┘When checking if user:alice can view document:readme, SpiceDB traverses:
document:readme#view→ followsviewerrelationgroup:engineering#member→ followsmemberrelation- Found
user:alice→ returns allowed
Each arrow represents one “hop” in the traversal. This tree has a depth of 3 (three nodes visited).
Traversal Depth Limit
To prevent unbounded traversal, SpiceDB enforces a maximum depth limit on every path traversed during a CheckPermission request.
By default, this limit is 50 hops.
If a traversal exceeds this limit, SpiceDB returns an error rather than continuing indefinitely.
You can configure this limit with the --dispatch-max-depth flag:
spicedb serve --dispatch-max-depth=100Most schemas work well within the default limit. You typically only need to increase it if you have legitimately deep hierarchies (like deeply nested folder structures).
Cyclical Relationships (Cycles)
A cycle occurs when traversing the permissions tree leads back to an object that was already visited. SpiceDB does not support cyclical relationships because the permissions graph must be a tree , not a graph with loops.
Example of a Cycle
Consider this schema for nested groups:
definition user {}
definition group {
relation member: user | group#member
}
definition resource {
relation viewer: user | group#member
permission view = viewer
}With these relationships:
resource:someresource#viewer@group:firstgroup#member
group:firstgroup#member@group:secondgroup#member
group:secondgroup#member@group:thirdgroup#member
group:thirdgroup#member@group:firstgroup#member ← creates a cycle!Visually, this creates a loop:
┌──────────────────────┐
│ resource:someresource│
│ permission: view │
└──────────┬───────────┘
│ viewer
▼
┌──────────────────────┐
│ group:firstgroup │◄─────────────────┐
│ permission: member │ │
└──────────┬───────────┘ │
│ member │
▼ │
┌──────────────────────┐ │
│ group:secondgroup │ │ member
│ permission: member │ │ (cycle!)
└──────────┬───────────┘ │
│ member │
▼ │
┌──────────────────────┐ │
│ group:thirdgroup │──────────────────┘
│ permission: member │
└──────────────────────┘When SpiceDB traverses this, it walks:
resource:someresource#viewer → group:firstgroup#member → group:secondgroup#member → group:thirdgroup#member → group:firstgroup#member → …
The traversal returns to group:firstgroup#member, creating an infinite loop.
How SpiceDB Handles Cycles
SpiceDB does not have a dedicated cycle detector. Instead, when a cycle exists, the traversal continues looping until it hits the maximum depth limit, then returns an error. This same error occurs whether the cause is a cycle or simply a very deep (but acyclic) hierarchy.
Why not track visited objects? SpiceDB intentionally avoids tracking visited objects for two reasons:
-
Semantic problems with self-referential sets: When a group’s members include itself, it creates logical paradoxes.
Consider this example:
definition user {} definition group { relation direct_member: user | group#member relation banned: user | group#member permission member = direct_member - banned }group:firstgroup#direct_member@group:secondgroup#member group:firstgroup#banned@group:bannedgroup#member group:secondgroup#direct_member@user:tom group:bannedgroup#direct_member@group:firstgroup#memberuser:tomis adirect_memberofsecondgroup, which makes him a member offirstgroup→ which implies he’s a member ofbannedgroup→ which implies he’s not a member offirstgroup→ thus making him no longerbanned→ (logical inconsistency). -
Performance overhead: Tracking every visited object would require significant memory and network overhead, especially in distributed deployments.
Common Questions
What do I do about a max depth error on CheckPermission?
If you see an error like:
the check request has exceeded the allowable maximum depth of 50: this usually indicates a recursive or too deep data dependency. Try running zed with --explain to see the dependencyUse zed permission check with --explain to visualize the traversal path:
zed permission check resource:someresource view user:someuser --explain1:36PM INF debugging requested on check
! resource:someresource viewer (4.084125ms)
└── ! group:firstgroup member (3.445417ms)
└── ! group:secondgroup member (3.338708ms)
└── ! group:thirdgroup member (3.260125ms)
└── ! group:firstgroup member (cycle) (3.194125ms)The output shows each hop in the traversal.
If you see (cycle) in the output, you have a cyclical relationship.
If there’s no cycle, your hierarchy is simply deeper than the limit allows.
Why did my check succeed despite having a cycle?
SpiceDB short-circuits CheckPermission when it finds the subject.
If the subject is found before the traversal hits the cycle or exceeds the depth limit, the check succeeds.
However, if the subject is not found, the traversal continues until it hits the depth limit and returns an error.
How do I prevent cycles when writing relationships?
Before writing a relationship that could create a cycle, use CheckPermission to verify the relationship won’t create a loop.
For example, before writing group:parent#member@group:child#member, check if the parent is already reachable from the child:
zed permission check group:child member group:parentIf this check returns allowed, writing the relationship would create a cycle. If it returns denied, the relationship is safe to write.
This pattern works because: if the parent already has permission on the child, making the child a member of the parent creates a circular dependency.