AgentFGA · Report · October 2026
Benchmarks.
Agents ask for permission before every tool call and on every document they retrieve, so each check has to be cheap. Here is how quickly AgentFGA answers, how much traffic it handles, and how it compares with OpenFGA. The tests ran against a deployed instance from a test machine in Australia.
§ 01 · Summary
The short version.
to answer a check on the server when the store is in the same data center as the request. About 21 ms from one region away.
in the largest store tested. Checks were as fast as in a store ten times smaller.
from one store. Past that, extra requests wait in line instead of failing.
across 16 stores, with zero errors. Capacity grows as you add customers.
These numbers are from 2026-10-18. A re-run a week later was slower: 12–30 ms per check and about 256 requests a second for one store. The stores landed in different data centers that time, and the older version measured the same day was slower too, so the code itself didn't regress. See the re-run.
§ 02 · Store size
Bigger stores aren't slower.
We filled three stores with a Google-Drive-like dataset of users, nested groups, folders and documents, then timed the same checks on each.
| Store | Relationships | Where it ran | Typical check |
|---|---|---|---|
| Small | 1,366 | Sydney, the same data center as the test traffic | 6 ms |
| Medium | 112,472 | Melbourne | 21 ms |
| Large | 1,124,085 | Melbourne | 21 ms |
Typical means the median, timed on the server. The trip from the test machine to the nearest data center added about 11 ms on top.
The medium and large stores differ tenfold in size and took the same time. The small store was quicker only because of where it was placed: the hop from Sydney to Melbourne adds about 15 ms. Every kind of check took the same time, from a direct grant to a walk through nested folders and groups.
Queries that return lists take longer. On the largest store, finding the 1,000 documents a user can view took 69 ms, and finding the 225 users who can view a document took 40 ms. Writing one relationship took 20–40 ms.
§ 03 · Throughput
How much traffic one store handles.
Each store runs as a single instance that works through requests one at a time. We sent a mix of checks to the largest store from more and more clients at once.
Requests answered per second
Largest store. No errors at any level.
Throughput levelled off at about 540 requests a second. More clients didn't cause errors; requests just waited longer, and the typical response time rose from 22 ms to 462 ms.
Batching raises the limit. BatchCheck sends up to 50 checks in one request. With 25 per request, one store answered about 3,600 checks a second.
§ 04 · Scaling
More stores, more capacity.
Stores never query each other, so each one runs on its own. We spread the same mix of checks across 1 to 16 small stores.
Requests answered per second
No errors in any run.
16 stores answered 6,799 requests a second. The limit was the single machine generating the traffic, not AgentFGA. In an agentic product, give each customer or workspace its own store.
§ 05 · Comparison
Compared with OpenFGA.
We ran AgentFGA's engine and OpenFGA v1.22.0 on the same PC, loaded the same 112,472 relationships into each, and timed the same queries.
| Typical time (median) | AgentFGA | OpenFGA |
|---|---|---|
| Check: direct owner | 0.01 ms | 1.9 ms |
| Check: owner counts as editor counts as viewer | 0.02 ms | 7.0 ms |
| Check: through 3 levels of groups | 0.04 ms | 31 ms |
| Check: through nested folders and groups | 0.15 ms | 38 ms |
| Check: denied, after searching everything | 0.12 ms | 37 ms |
| Check: user is blocked | 0.20 ms | 1.7 ms |
| Check: time-limited access | 0.02 ms | 1.1 ms |
| BatchCheck: 25 checks | 18 ms¹ | 683 ms |
| ListObjects: 1,000 documents | 9.7 ms | 360 ms |
| ListUsers: 186 users | 17 ms | 25 ms |
OpenFGA on its SQLite database with default settings. ¹ Timed inside the local development server (wrangler dev).
Not a perfect like-for-like test. AgentFGA was timed inside its own process, while OpenFGA was timed over a local HTTP connection, which adds about 0.5 ms. OpenFGA's recommended production database, Postgres, wasn't tested, so OpenFGA may be faster in production than shown here.
OpenFGA does better in two places. With its optional caches turned on, it answered repeated ListObjects queries faster (about 6 ms against 10 ms), though cached answers can be up to 10 seconds old. And one OpenFGA process uses every CPU core, while one AgentFGA store handles one request at a time; AgentFGA gets more capacity by adding stores instead.
§ 06 · Data
Full results.
Every number comes from the test harness in bench/. The complete write-up, including the performance fixes these tests led to, is in BENCHMARKS.md.
Every query, by store size
Client is the full round trip from the test machine in Australia. Server is measured from the moment the request reached the nearest data center. Median of 40 requests each, measured 2026-10-18.
| Median, ms | small · Sydney | medium · Melbourne | large · Melbourne | |||
|---|---|---|---|---|---|---|
| client | server | client | server | client | server | |
| Check: direct owner | 17 | 6 | 33 | 21 | 32 | 21 |
| Check: owner → editor → viewer | 17 | 6 | 32 | 21 | 31 | 20 |
| Check: 3 levels of groups | 17 | 6 | 31 | 21 | 31 | 21 |
| Check: nested folders and groups | 17 | 6 | 30 | 21 | 32 | 21 |
| Check: denied | 17 | 6 | 31 | 20 | 31 | 21 |
| Check: user is blocked | 17 | 6 | 31 | 21 | 32 | 21 |
| Check: time-limited access | 17 | 6 | 32 | 21 | 32 | 21 |
| BatchCheck: 25 checks | 22 | 10 | 45 | 31 | 51 | 39 |
| ListObjects: a user's documents 300 · 1,000 · 1,000 results | 32 | 19 | 81 | 63 | 87 | 69 |
| ListObjects: public documents 0 · 1,000 · 1,000 results | 18 | 6 | 76 | 61 | 84 | 69 |
| ListUsers: a shared document 151 · 186 · 225 users | 33 | 19 | 50 | 36 | 55 | 40 |
| Expand: a document's viewers | 17 | 6 | 32 | 20 | 31 | 20 |
| Read: one document's relationships | 18 | 6 | 32 | 21 | 31 | 21 |
| Write: 1 relationship | 31 | 20 | 49 | 38 | 48 | 37 |
One store under load
The largest store, with clients sending requests back to back for 8–10 seconds. Half the checks are for a document's owner and half for a random user, which forces a full search.
| Clients at once | Requests/s | Client median / slowest 1%, ms | Server median / slowest 1%, ms | Errors |
|---|---|---|---|---|
| 1 | 27 | 33 / 183 | 22 / 35 | 0 |
| 8 | 210 | 37 / 64 | 24 / 40 | 0 |
| 32 | 406 | 79 / 121 | 64 / 98 | 0 |
| 64 | 441 | 138 / 331 | 110 / 210 | 0 |
| 128 | 540 | 239 / 334 | 214 / 313 | 0 |
| 256 | 512 | 491 / 734 | 462 / 713 | 0 |
Even a trivial request that reads nothing topped out at about 680–700 per second, so the limit is the single store instance itself, not the permission logic. With BatchCheck (25 checks per request):
| Clients at once | Batches/s | Checks/s | Server median, ms |
|---|---|---|---|
| 4 | 75 | 1,875 | 38 |
| 8 | 95 | 2,375 | 69 |
| 16 | 121 | 3,025 | 117 |
| 64 | 145 | 3,625 | 429 |
Many stores
| Stores | Clients at once | Requests/s | Client median, ms | Server median, ms | Errors |
|---|---|---|---|---|---|
| 1 | 256 | 705 | 363 | 335 | 0 |
| 2 | 256 | 1,229 | 211 | 180 | 0 |
| 4 | 256 | 2,326 | 108 | 91 | 0 |
| 8 | 256 | 3,436 | 70 | 53 | 0 |
| 16 | 256 | 4,381 | 54 | 37 | 0 |
| 16 | 1,024 | 6,799 | 141 | 114 | 0 |
Every comparison with OpenFGA
Same PC (AMD Ryzen 7 7700, Windows 11), 40 timed requests per query after warm-up, measured 2026-10-26. OpenFGA's caches answer repeated queries for up to 10 seconds, so cached results can be out of date.
| Median, ms | AgentFGA | OpenFGA SQLite | OpenFGA in-memory | OpenFGA SQLite + caches |
|---|---|---|---|---|
| Check: direct owner | 0.01 | 1.9 | 2.6 | 3.0 |
| Check: owner → editor → viewer | 0.02 | 7.0 | 9.0 | 5.7 |
| Check: 3 levels of groups | 0.04 | 31 | 37 | 10 |
| Check: nested folders and groups | 0.15 | 38 | 86 | 9.2 |
| Check: denied | 0.12 | 37 | 42 | 26 |
| Check: user is blocked | 0.20 | 1.7 | 5.7 | 1.6 |
| Check: time-limited access | 0.02 | 1.1 | 4.4 | 1.3 |
| BatchCheck: 25 checks | 18¹ | 683 | 665 | 890 |
| ListObjects: a user's documents 1,000 results | 9.7 | 360 | 91 | 5.9 |
| ListObjects: public documents 1,000 results | 10 | 198 | 58 | 5.3 |
| ListUsers: a shared document 186 results | 17 | 25 | 37 | 31 |
| Write: 1 relationship | – | 5.8 | 42 | 5.7 |
Bold marks the fastest in each row. ¹ Timed inside wrangler dev.
Under the same mix of checks used in the deployed tests, one OpenFGA process on this PC handled 10–47 requests a second, and from 32 clients up some requests ran past its 3-second deadline. It managed 600–820 a second when every check was a simple direct one.
The re-run on 2026-10-26
After the compatibility work, the final version was deployed as a new instance and measured again. This time the small store landed in Brisbane, about 25 ms from the Sydney data center the traffic entered, and the large one in Sydney.
| Server median | small · Brisbane | medium | large · Sydney |
|---|---|---|---|
| Checks | 25–30 ms | 23–24 ms | 12–15 ms |
| BatchCheck: 25 | 37 | 35 | 60 |
| ListObjects: 1,000 results | 50² | 51–52 | 64–70 |
| ListUsers | 90 | 57 | 89 |
| Write: 1 relationship | 45 | 38 | 28 |
² 300 results on the small store.
The Sydney hop itself was slower that day for both versions: the old version's Sydney store took 10–13 ms per request, against 6 ms a week earlier.
| One store (large, Sydney) | 2026-10-18 | 2026-10-26 |
|---|---|---|
| Mix of checks, requests/s | 540 | 256 |
| BatchCheck of 25, checks/s | 3,625 | 1,400 |
| Trivial request, requests/s | 680–700 | 394–511 |
A same-day comparison on fresh stores shows the new version isn't the cause:
| 2026-10-26 | old version | new version |
|---|---|---|
| Trivial request, requests/s | 405–410 | 503–511 |
| Bulk loading, relationships/s | 4,206 | 6,791 |
Cold starts
The first request after a deploy, or after 4 minutes idle, compared with a warm request.
| Client / server, ms | small | medium | large |
|---|---|---|---|
| First request after a deploy | 91 / 47 | 80 / 66 | 76 / 63 |
| First request after 4 minutes idle | 100 / 35 | 79 / 63 | 83 / 65 |
| Warm request | 21 / 7 | 31 / 21 | 32 / 21 |
Test data and how to reproduce
A Google-Drive-like model (bench/dataset.ts), generated from a fixed seed at three sizes:
- Groups nested 3 levels deep.
- Folders nested 4 levels deep, whose viewers and editors carry down to what's inside.
- Documents with owners, and access granted directly or through groups.
- Blocked users, and time-limited access checked with a condition.
| Store | Relationships | Users | Groups | Folders | Documents |
|---|---|---|---|---|---|
| small | 1,366 | 200 | 20 | 50 | 300 |
| medium | 112,472 | 10k | 1k | 4k | 30k |
| large | 1,124,085 | 100k | 10k | 40k | 300k |
.bench.env holds AGENTFGA_URL and AGENTFGA_KEY and is git-ignored.
set -a && . ./.bench.env && set +a
npx tsx bench/load.ts small|medium|large [concurrency] [copies] # create + bulk-load stores
npx tsx bench/run.ts latency small medium large # per-query latency
npx tsx bench/run.ts throughput large 1,8,32,64,128,256 10 # one store under load
BENCH_WORKLOAD=batch25 npx tsx bench/run.ts throughput large 4,8,16,64 8
npx tsx bench/run.ts horizontal small 1,2,4,8,16 256 8 # needs: bench/load.ts small 4 16
npx tsx bench/run.ts cold 240 # first request after idle
npx tsx bench/engine.ts # engine only, local node:sqlite
# The same suites against another server, e.g. OpenFGA on :18090:
BENCH_URL=http://127.0.0.1:18090 BENCH_STORES=bench/results/stores-upstream.json npx tsx bench/load.ts medium 8
BENCH_URL=http://127.0.0.1:18090 BENCH_STORES=bench/results/stores-upstream.json npx tsx bench/run.ts latency medium