1. Docs
  2. Authorization caching

Authorization Caching

How an authorization check is answered inside your own process, what the SDK holds to do it, and the window between changing someone's access and that change taking effect.

How a check is answered

A guarded route does not call Canopy. The SDK fetches two things and keeps them: where an identity holds each permission, and the shape of your hierarchy, and every check after that is a walk up the tree in your own memory. One call per identity per minute replaces one call per request, and your request path no longer waits on ours.

What is cached, and where

Two caches, split by what they depend on. Both live inside your process; Canopy stores nothing extra on your behalf.

Grant roots, per identity. Each permission the identity holds, and the nodes it was granted at. Small by construction: a person has a handful of role assignments, not a handful of thousands.The hierarchy, once per process. Node to parent, nothing else, shared by every identity your server handles. The shape of your tree does not depend on who is asking, so it is not held per user.

A grant already means this node and everything beneath it, so the grant roots are never expanded into their descendants. That is what keeps the per-identity cache small even for an administrator granted at the top of a large tree, and it is why a check is a walk rather than a lookup: the SDK climbs from the node in question and asks whether any ancestor is one of the identity's grant roots. Measured, the hierarchy costs about 74 bytes per node once V8 has amortised the map: 3.6 MB for a 50,000-node tenant, against the 50–100 MB an idle Node process already holds. It is shared across every identity your server handles, so it does not grow with your user count.

The sixty-second window

An authorization change takes effect within 60 seconds: sometimes sooner, never later.

Which means the thing worth saying plainly: after you revoke someone's access, they can still act for up to a minute.

The window covers every change that can alter an answer:

A role assignment created, rescheduled, or removed.A role's permissions changed, or the role deleted.An identity deactivated, or its Environment membership revoked.A node moved. This one is easy to overlook: moving a node changes what every inherited grant reaches, even though no grant itself changed. The SDK revalidates the hierarchy on the same cadence for exactly this reason.

Why permissions are not in the token

A reasonable question, especially coming from a provider that does it: why not put the permissions in the access token and skip the fetch entirely? Two things prevent it.

A token cannot be invalidated. It is a snapshot signed at login. Revoking access cannot reach it: you would wait out the token's own lifetime, which is not yours to set per check. A cache's window is.A token cannot carry your hierarchy. It travels on every request, so its size is the binding constraint. Without the tree, a token can never answer at this node, only whether the permission is held somewhere.

Canopy does emit an Application-wide permissions claim for OAuth clients that request the permissions scope, which is useful if you integrate with a stock OIDC library and only need the loose question answered. The SDK deliberately does not read it: two sources for one answer, with two different staleness windows, is worse than one.

Without the SDK

None of this is SDK-only. Both endpoints are ordinary HTTP, and the algorithm is short enough to write yourself:

Read GET /api/v1/identities/{id}/grants for the identity, and keep it for up to a minute.Read GET /api/v1/nodes once for the process, and keep it.Revalidate the tree with If-None-Match carrying the ETag from that response. A 304 means your copy is still good; anything else means refetch.To answer a check, climb from the node in question through its parents and return true if any of them appears in that permission's grant roots.

That is the entire mechanism. The SDK adds the caching, the revalidation timer and the failure handling, but nothing about it is privileged.

Serverless and edge

The design assumes a long-running process. On a serverless function or at the edge, every cold start begins with an empty cache and refetches both the grant roots and the tree. It still works, and it is still far better than a call per request, but the saving is smaller and it surfaces as slower cold starts rather than as steady-state latency.

Tuning

The window is yours to shorten, and there is one trap worth knowing before you do.

A window shorter than the gap between a user's requests saves nothing. Every request finds the cache expired and refetches, which is a call per request, the very thing the cache exists to avoid. Human-paced traffic has multi-second gaps between clicks, so a window of a few seconds can cost full price for no benefit.

If you need revocation faster than the window allows, that is not a tuning question. Revoke the identity's sessions as well: an access token that has been revoked stops working at its own expiry, and deactivating the identity denies it at the next refresh regardless of what any cache holds.

Environment
API version
v1.0
On this page Was this page helpful?

Tell us how we can improve this guide.