Engineering Sub-10ms Authorization Checks
Authorization runs on the critical path of every request, so its p99 latency is your p99 latency. Four techniques — colocating the PDP, denormalizing decision data, request-scoped caching, and batching — for keeping access checks under 10ms.
Authorization runs on the critical path of every single request. That means your authorization p99 is a floor under your entire API’s p99 — if an access check takes 80ms, no endpoint can be faster than 80ms. Getting checks under 10ms is not a vanity metric; it is a prerequisite for calling authorization on every request the way zero trust demands.
Where the Milliseconds Go
- Network hops to a distant PDP or database.
- Joins / fan-out — fetching a policy, then roles, then permissions in sequence.
- Attribute fetches in ABAC, from slow Policy Information Points.
- Cold starts on serverless PDPs.
Technique 1: Colocate the PDP
Every network hop between your service and the decision point adds round-trip latency. Run the PDP in the same region — ideally the same VPC — as your application. A cross-continent hop alone can cost 100–150ms; same-region is sub-millisecond.
Technique 2: Denormalize the Decision Data
The RBAC evaluation path is naturally a graph (subject → roles → permissions). Sequential lookups multiply latency. Store the data so a decision needs the fewest possible round trips — this is exactly why we chose single-table DynamoDB design, which keeps all of a tenant’s data in one partition and resolves a check in three operations against the same node.
Technique 3: Request-Scoped Caching
A single request often checks the same permission several times (render, then mutate). Deduplicate within the request lifecycle so you pay for the decision once:
import { cache } from 'react'
// Deduplicated per request — same args resolve to one PDP call.
export const checkPermission = cache(async (userId, scopeId, resource, action) => {
const { allowed } = await pdp.hasAccess({ subjectId: userId, scopeId, resource, action })
return allowed
})Technique 4: Batch Checks
Rendering a dashboard often needs many decisions at once. Batch them into one call instead of N sequential round trips:
const decisions = await pdp.batchHasAccess([
{ subjectId, scopeId, resource: 'invoice', action: 'create' },
{ subjectId, scopeId, resource: 'invoice', action: 'delete' },
{ subjectId, scopeId, resource: 'report', action: 'export' },
])Measure, Don’t Guess
const start = performance.now()
const { allowed } = await pdp.hasAccess(req)
metrics.histogram('authz.latency_ms', performance.now() - start)Track p50/p95/p99 separately. The tail is what your users feel.
How WardenAuth Hits Sub-10ms
WardenAuth combines single-table storage, a colocated evaluation path, and deny-wins resolution in a single query pattern — typical decisions land in 3–8ms end to end. See it for yourself.