WardenAuthAgent Security
PricingDocsCompareBlogLearnChangelog
Sign inGet started free
All posts
Architecture
July 21, 2025
11 min read

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:

typescript
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
})
Cross-request caching trades correctness for speed: a revoked role stays “allowed” until the cache expires. Keep TTLs short and never cache security-critical actions (payments, deletes).

Technique 4: Batch Checks

Rendering a dashboard often needs many decisions at once. Batch them into one call instead of N sequential round trips:

typescript
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

typescript
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.


Back to blogTry WardenAuth free →
© 2026 ecarrizo. All rights reserved.
PricingDocsCompareBlogLearnChangelogStatusGlossaryContactTermsPrivacy