Skip to content

Benchmarks dashboard

The page draws every chart at build time from the run files. Each chart has a table view under it. The design colors are the same everywhere: A tuple core, B policy first, C compiled model. The engine comparison report is the prose summary of these numbers. The decision record explains how these measurements informed the architecture.

Dataset
PG 18 (docker, optimized)
PG 18.6 (docker, optimized)
Runs
14
5 configurations, 96 runs in all series
Latest run
2026-11-02
first 2026-10-29
Checksums agree
3 of 3
configurations with identical visible ids on all engines

The charts use PG 18 (docker, optimized) on Apple M4, 10 CPU, 24 GB; container cpus=4 mem=6g: Postgres 18.6, the latest run for each design, scale and shape. This is the primary series, and we chose it on purpose. It is Docker, the newest Postgres version, and the optimized designs when their runs exist (otherwise the newest Docker series, then local). Cedar (E) comes from its own tagged run (tag p9). 4 pgbench clients, 10 s for each workload. Docker runs cover only the large default, medium deep and medium wide configurations. A figure never mixes environments or tags. The data has these series: PG 16 (docker); PG 16 (docker, optimized); PG 17 (docker); PG 17 (docker, optimized); PG 18 (docker); PG 18 (docker, optimized); PG 18 (docker, opt2); PG 18 (docker, opt3); PG 18 (docker, p5); PG 18 (docker, p6); PG 18 (docker, p6check); PG 18 (docker, p9); PG 16 (local). Compare Postgres versions on the Postgres versions page. Compare the runs before and after the optimization pass on the Optimizations page. All latencies are in milliseconds. There is one run for each configuration, so differences under about 20% are noise. The run files are in bench/baselines/data.

Latency by workload and design

The bar is the p50 and the whisker is the p95, in milliseconds on a log axis. Pick a data scale and a team shape.

  • A Tuple core
  • B Policy first
  • C Compiled model
  • D Per-model tables
  • E Cedar
  • p95 whisker

large default

0.10.1111010100100ms, log scaleRBACSingle checkrbac_checkA0.30B0.27C0.24D0.22First pagerbac_select_pageA0.76B0.73C0.67D0.72Count visiblerbac_select_countA0.78B0.78C0.73D0.77Update one rowrbac_update_oneA0.47B0.47C0.36D0.28Assign a rolewrite_assign_roleA0.15B0.14C0.17D0.14SharingList visible idsshare_list_objectsA0.29B0.29C0.25D0.54First pageshare_select_pageA1.03B1.00C0.93D1.29Share one documentwrite_shareA0.15B0.14C0.17D0.23Parents, projection policyFirst pageparents_proj_select_pageA1.33B1.15C1.05D1.66Count visibleparents_proj_select_countA8.42B8.20C7.88D8.92Parents, single-branch policyFirst pageparents_select_pageA45.6B13.7C4.94D1.40Count visibleparents_select_countA75.1B21.6C11.9D8.44NestingNest a teamwrite_nest_teamA0.15B0.62C57.7D0.24Check through nested teamnest_checkA0.31B0.32C0.30D0.41
Table view: large default
WorkloadA p50B p50C p50D p50A p95B p95C p95D p95
RBAC: single check0.300.270.240.220.560.410.330.36
RBAC: first page0.760.730.670.721.441.100.911.81
RBAC: count visible0.780.780.730.771.070.990.931.55
RBAC: update one row0.470.470.360.280.650.680.500.44
RBAC: assign a role0.150.140.170.140.230.220.240.22
Sharing: list visible ids0.290.290.250.540.380.430.360.71
Sharing: first page1.031.000.931.291.351.291.273.34
Sharing: share one document0.150.140.170.230.230.210.250.35
Parents, projection: first page1.331.151.051.662.621.551.572.61
Parents, projection: count visible8.428.207.888.9214.712.49.9711.0
Parents, single branch: first page45.613.74.941.4066.316.86.559.45
Parents, single branch: count visible75.121.611.98.4492.427.615.516.0
Nesting: nest a team0.150.6257.70.240.270.7877.20.50
Nesting: check through a nested team0.310.320.300.410.480.420.450.64

medium default

0.10.11110101001001k1kms, log scaleRBACSingle checkrbac_checkE0.31First pagerbac_select_pageE0.49Count visiblerbac_select_countE0.40Update one rowrbac_update_oneE0.58Assign a rolewrite_assign_roleE0.26SharingList visible idsshare_list_objectsE0.39First pageshare_select_pageE0.79Share one documentwrite_shareE0.26Parents, projection policyFirst pageparents_proj_select_pageE0.93Count visibleparents_proj_select_countE1.27Parents, single-branch policyFirst pageparents_select_pageE0.84Count visibleparents_select_countE1.19NestingNest a teamwrite_nest_teamE1.17Check through nested teamnest_checkE0.37Per rowFirst page (every row checked)row_select_pageE97.5
Table view: medium default
WorkloadE p50E p95
RBAC: single check0.310.47
RBAC: first page0.490.70
RBAC: count visible0.400.61
RBAC: update one row0.581.08
RBAC: assign a role0.260.42
Sharing: list visible ids0.390.62
Sharing: first page0.791.30
Sharing: share one document0.260.47
Parents, projection: first page0.931.61
Parents, projection: count visible1.272.23
Parents, single branch: first page0.841.88
Parents, single branch: count visible1.192.41
Nesting: nest a team1.171.76
Nesting: check through a nested team0.370.52
Per row: first page, every row checked (Cedar only)97.5139

small default

0.10.1111010100100ms, log scaleRBACSingle checkrbac_checkE0.30First pagerbac_select_pageE0.37Count visiblerbac_select_countE0.32Update one rowrbac_update_oneE0.52Assign a rolewrite_assign_roleE0.26SharingList visible idsshare_list_objectsE0.35First pageshare_select_pageE0.56Share one documentwrite_shareE0.26Parents, projection policyFirst pageparents_proj_select_pageE0.67Count visibleparents_proj_select_countE0.66Parents, single-branch policyFirst pageparents_select_pageE0.59Count visibleparents_select_countE0.57NestingNest a teamwrite_nest_teamE1.10Check through nested teamnest_checkE0.35Per rowFirst page (every row checked)row_select_pageE35.1
Table view: small default
WorkloadE p50E p95
RBAC: single check0.300.52
RBAC: first page0.370.69
RBAC: count visible0.320.61
RBAC: update one row0.520.84
RBAC: assign a role0.260.45
Sharing: list visible ids0.350.50
Sharing: first page0.560.90
Sharing: share one document0.260.62
Parents, projection: first page0.671.15
Parents, projection: count visible0.661.06
Parents, single branch: first page0.590.99
Parents, single branch: count visible0.570.83
Nesting: nest a team1.101.81
Nesting: check through a nested team0.350.63
Per row: first page, every row checked (Cedar only)35.147.5

medium deep

0.10.11110101001001k1kms, log scaleRBACSingle checkrbac_checkA0.29B0.27C0.24D0.22First pagerbac_select_pageA0.44B0.46C0.42D0.41Count visiblerbac_select_countA0.35B0.37C0.32D0.32Update one rowrbac_update_oneA0.48B0.46C0.35D0.28Assign a rolewrite_assign_roleA0.15B0.14C0.18D0.14SharingList visible idsshare_list_objectsA0.31B0.32C0.28D0.78First pageshare_select_pageA0.76B0.72C0.66D1.18Share one documentwrite_shareA0.15B0.14C0.17D0.23Parents, projection policyFirst pageparents_proj_select_pageA1.00B0.83C0.73D1.79Count visibleparents_proj_select_countA1.25B1.14C0.99D2.09Parents, single-branch policyFirst pageparents_select_pageA7.41B4.52C2.83D1.23Count visibleparents_select_countA6.40B3.74C2.10D1.54NestingNest a teamwrite_nest_teamA0.15B0.63C75.1D0.23Check through nested teamnest_checkA0.34B0.34C0.30D0.43
Table view: medium deep
WorkloadA p50B p50C p50D p50A p95B p95C p95D p95
RBAC: single check0.290.270.240.220.400.360.320.36
RBAC: first page0.440.460.420.410.560.570.570.70
RBAC: count visible0.350.370.320.320.480.480.410.53
RBAC: update one row0.480.460.350.280.660.580.460.45
RBAC: assign a role0.150.140.180.140.230.210.240.21
Sharing: list visible ids0.310.320.280.780.410.400.371.02
Sharing: first page0.760.720.661.181.000.890.901.50
Sharing: share one document0.150.140.170.230.220.200.230.31
Parents, projection: first page1.000.830.731.791.261.141.002.74
Parents, projection: count visible1.251.140.992.091.471.401.182.84
Parents, single branch: first page7.414.522.831.239.765.543.802.40
Parents, single branch: count visible6.403.742.101.548.066.012.692.52
Nesting: nest a team0.150.6375.10.230.230.731150.31
Nesting: check through a nested team0.340.340.300.430.470.440.410.54

medium wide

0.10.1111010100100ms, log scaleRBACSingle checkrbac_checkA0.29B0.28C0.24D0.21First pagerbac_select_pageA0.44B0.46C0.44D0.40Count visiblerbac_select_countA0.36B0.36C0.33D0.31Update one rowrbac_update_oneA0.49B0.48C0.38D0.27Assign a rolewrite_assign_roleA0.15B0.14C0.18D0.14SharingList visible idsshare_list_objectsA4.44B8.41C3.41D17.8First pageshare_select_pageA14.4B17.8C8.20D26.2Share one documentwrite_shareA0.15B0.16C0.17D0.23Parents, projection policyFirst pageparents_proj_select_pageA15.0B15.1C8.73D38.6Count visibleparents_proj_select_countA13.2B13.4C6.33D36.1Parents, single-branch policyFirst pageparents_select_pageA14.7B13.7C8.06D23.6Count visibleparents_select_countA13.3B12.2C6.61D21.6NestingNest a teamwrite_nest_teamA0.15B1.47C61.2D0.23Check through nested teamnest_checkA0.37B0.34C0.31D0.45
Table view: medium wide
WorkloadA p50B p50C p50D p50A p95B p95C p95D p95
RBAC: single check0.290.280.240.210.400.370.350.31
RBAC: first page0.440.460.440.400.560.590.610.61
RBAC: count visible0.360.360.330.310.480.450.430.46
RBAC: update one row0.490.480.380.270.630.950.490.48
RBAC: assign a role0.150.140.180.140.230.240.260.25
Sharing: list visible ids4.448.413.4117.85.5715.83.8737.1
Sharing: first page14.417.88.2026.217.024.216.549.5
Sharing: share one document0.150.160.170.230.220.400.240.32
Parents, projection: first page15.015.18.7338.618.321.718.851.1
Parents, projection: count visible13.213.46.3336.116.518.17.1742.5
Parents, single branch: first page14.713.78.0623.619.220.010.734.9
Parents, single branch: count visible13.312.26.6121.616.617.57.4827.6
Nesting: nest a team0.151.4761.20.230.211.8173.30.30
Nesting: check through a nested team0.370.340.310.450.480.610.440.96

Takeaway. At large/default: the RBAC workloads are equal on all designs (rbac_check p50 0.22 to 0.30 ms across designs); the single-branch parents page takes 1.40 ms on D and 45.6 ms on A (33x); a nest of a team takes 0.15 ms on A but 57.7 ms on C. The designs differ on sharing, parents and nesting, not on RBAC.

Raw report: COMPARISON.md (local PG 16, before optimization), per-run reports

Scale from 10k to 1M documents

PG 18 (docker, optimized): p50 latency, default shape, log axis. The x axis is the number of documents in the table.

  • A Tuple core
  • B Policy first
  • C Compiled model
  • D Per-model tables
  • E Cedar

RBAC: single check

rbac_check

0.10.20.5110k100k1ME 0.31C 0.24

Sharing: first page

share_select_page

0.10.20.51251010k100k1MD 1.3C 0.93

Parents, projection: first page

parents_proj_select_page

0.10.20.51251010k100k1MD 1.7B 1.1

Parents, single branch: first page

parents_select_page

0.111010010k100k1MA 46B 14C 4.9D 1.4E 0.84

Nesting: nest a team

write_nest_team

0.111010010k100k1MC 58E 1.2B 0.62D 0.24A 0.15

Nesting: check through a nested team

nest_check

0.10.20.5110k100k1MD 0.41B 0.32

Takeaway. From 10k to 1M documents, the sharing page grows slowly (). The single-branch parents page follows the number of visible ids and grows with it (). The RBAC workloads stay almost flat.

Table view
Workload (p50 ms)A smallA mediumA largeB smallB mediumB largeC smallC mediumC largeD smallD mediumD largeE smallE mediumE large
RBAC: single checkn/an/a0.30n/an/a0.27n/an/a0.24n/an/a0.220.300.31n/a
Sharing: first pagen/an/a1.03n/an/a1.00n/an/a0.93n/an/a1.290.560.79n/a
Parents, projection: first pagen/an/a1.33n/an/a1.15n/an/a1.05n/an/a1.660.670.93n/a
Parents, single branch: first pagen/an/a45.6n/an/a13.7n/an/a4.94n/an/a1.400.590.84n/a
Nesting: nest a teamn/an/a0.15n/an/a0.62n/an/a57.7n/an/a0.241.101.17n/a
Nesting: check through a nested teamn/an/a0.31n/an/a0.32n/an/a0.30n/an/a0.410.350.37n/a

Raw report: COMPARISON.md (local PG 16)

Default, deep and wide team shapes (medium data)

PG 18 (docker, optimized): p50 latency, log axis. In deep, the teams are in chains 9 levels deep. In wide, each user is in 50 teams and each team has 50 shares.

  • A Tuple core
  • B Policy first
  • C Compiled model
  • D Per-model tables
  • E Cedar

Sharing: first page

share_select_page

0.11101000.79default0.760.720.661.2deep14188.226wide

Sharing: list visible ids

share_list_objects

0.11101000.39default0.310.320.280.78deep4.48.43.418wide

Parents, projection: first page

parents_proj_select_page

0.11101000.93default1.00.830.731.8deep15158.739wide

Nesting: check through a nested team

nest_check

0.10.20.510.37default0.340.340.300.43deep0.370.340.310.45wide

Takeaway. Medium data, PG 18 (docker, optimized): in the wide shape the designs differ most: A 14.4, B 17.8, C 8.20 ms (1.8x between A and C).

Table view
Workload (p50 ms)A defaultB defaultC defaultD defaultE defaultA deepB deepC deepD deepE deepA wideB wideC wideD wideE wide
Sharing: first pagen/an/an/an/a0.790.760.720.661.18n/a14.417.88.2026.2n/a
Sharing: list visible idsn/an/an/an/a0.390.310.320.280.78n/a4.448.413.4117.8n/a
Parents, projection: first pagen/an/an/an/a0.931.000.830.731.79n/a15.015.18.7338.6n/a
Nesting: check through a nested teamn/an/an/an/a0.370.340.340.300.43n/a0.370.340.310.45n/a

Raw report: COMPARISON.md (local PG 16, before optimization)

Read cost against write cost

PG 18 (docker, optimized). Each point is one design. The x axis is the read of the sharing page (share_select_page p50). The y axis is a nest of a team of 1,000 members (write_nest_team p50). Both axes are in ms on a log scale. A point nearer the bottom left is better.

  • A Tuple core
  • B Policy first
  • C Compiled model
  • D Per-model tables
  • E Cedar

Medium, wide shape

0.11101000.1110100read: share_select_page p50 (ms)write: write_nest_team p50 (ms)ABCD

Large, default shape

0.11101000.1110100read: share_select_page p50 (ms)write: write_nest_team p50 (ms)ABCD

Takeaway. No design wins on both axes. In the medium, wide shape, A reads the sharing page in 14.4 ms and nests a team in 0.15 ms. C reads in 8.20 ms (1.8x faster), but a nest costs 61.2 ms (410x slower), because C writes membership rows under a global lock.

Table view
ConfigurationDesignshare_select_page p50write_nest_team p50
Medium, wide shapeA14.40.15
Medium, wide shapeB17.81.47
Medium, wide shapeC8.2061.2
Medium, wide shapeD26.20.23
Large, default shapeA1.030.15
Large, default shapeB1.000.62
Large, default shapeC0.9357.7
Large, default shapeD1.290.24

Raw report: COMPARISON.md (local PG 16, before optimization: Nesting)

Parents: two-arm projection against single-branch policy

PG 18 (docker, optimized): first page p50, default shape, ms on a log axis shared by the 3 panels.

  • A Tuple core
  • B Policy first
  • C Compiled model
  • D Per-model tables
  • E Cedar

Small (10k documents)

0.11101000.67Projection0.59Single branch

Medium (100k documents)

0.11101000.93Projection0.84Single branch

Large (1M documents)

0.11101001.31.11.11.7Projection46144.91.4Single branch

Takeaway. The policy shape costs more than the design. At 1M documents the single-branch page is 34x slower than the projection on A (1.33 to 45.6 ms), 12x on B and 4.7x on C (1.05 to 4.94 ms). The reason is that the design must materialize every visible id.

Table view
ScalePolicyA p50B p50C p50D p50E p50
smallprojectionn/an/an/an/a0.67
smallsingle branchn/an/an/an/a0.59
mediumprojectionn/an/an/an/a0.93
mediumsingle branchn/an/an/an/a0.84
largeprojection1.331.151.051.66n/a
largesingle branch45.613.74.941.40n/a

Raw report: COMPARISON.md (local PG 16, before optimization: policy shape)

Seed time and storage, per phase

Left: native-API load time for each phase (seconds, log axis). Right: size of the permissions tables after each phase (cumulative MB, after VACUUM FULL). Proj. is parents seeded through the folder_id projection. Links is a parent row for each document.

  • A Tuple core
  • B Policy first
  • C Compiled model
  • D Per-model tables
  • E Cedar

large default

Seed time

seconds per phase, log axis

0.010.11101005.14.35.04.9RBAC1.01.91.8SharingProj.31451834LinksNesting

Storage

cumulative MB after each phase

0200400600RBACSharingProj.LinksNestingA 404D 285C 183
Table view: large default
PhaseA seed sB seed sC seed sD seed sA MBB MBC MBD MB
RBAC5.144.314.954.867.007.405.305.40
Sharing0.651.001.931.8418.719.621.316.1
Parents (proj.)0.080.110.340.1019.620.622.316.8
Parents (links)31.044.817.633.8404378182285
Nesting0.050.020.040.03404378183285

medium default

Seed time

seconds per phase, log axis

0.010.11100.80RBAC8.2Sharing0.15Proj.3.9Links0.02Nesting

Storage

cumulative MB after each phase

020406080RBACSharingProj.LinksNestingE 67
Table view: medium default
PhaseE seed sE MB
RBAC0.804.20
Sharing8.1940.8
Parents (proj.)0.1542.2
Parents (links)3.9066.7
Nesting0.0266.7

small default

Seed time

seconds per phase, log axis

0.010.11100.10RBAC1.1Sharing0.08Proj.0.54Links0.02Nesting

Storage

cumulative MB after each phase

051015RBACSharingProj.LinksNestingE 11
Table view: small default
PhaseE seed sE MB
RBAC0.101.50
Sharing1.157.80
Parents (proj.)0.088.10
Parents (links)0.5410.6
Nesting0.0210.6

medium deep

Seed time

seconds per phase, log axis

0.010.1110RBAC1.1SharingProj.2.14.01.63.2LinksNesting

Storage

cumulative MB after each phase

0204060RBACSharingProj.LinksNestingA 49D 36
Table view: medium deep
PhaseA seed sB seed sC seed sD seed sA MBB MBC MBD MB
RBAC0.600.480.810.463.603.902.903.00
Sharing0.360.481.080.899.5010.115.08.70
Parents (proj.)0.040.060.150.0510.010.615.59.00
Parents (links)2.143.971.573.1848.646.531.535.9
Nesting0.020.020.060.0248.646.533.135.9

medium wide

Seed time

seconds per phase, log axis

0.0010.010.1110100RBAC5.9132513SharingProj.2.3131.63.2LinksNesting

Storage

cumulative MB after each phase

0100200300RBACSharingProj.LinksNestingC 209B 142D 109
Table view: medium wide
PhaseA seed sB seed sC seed sD seed sA MBB MBC MBD MB
RBAC0.580.510.840.453.603.902.903.00
Sharing5.8812.624.713.397.410519281.4
Parents (proj.)0.040.350.200.0597.910619281.8
Parents (links)2.3213.01.653.18136142208109
Nesting0.040.020.040.01136142209109

Takeaway. Per-document parent links are the largest part of both costs at large/default: A 31.0 s and +385 MB, B 44.8 s and +357 MB, C 17.6 s and +160 MB, D 33.8 s and +268 MB. The two-arm projection needs none of them. The other phases are small in comparison.

Raw report: COMPARISON.md (local PG 16, before optimization: storage and seed cost)

The names and the descriptions come from the header of each workload SQL file in pg_permissions/bench/workloads/ and pg_permissions/bench/designs/c/workloads/. The three designs use the same workload names and the same intent.

WorkloadWhat it measuresKind
rbac_check
RBAC: single check
One permission check in the user's home org (the per-row path for writes).read
rbac_select_page
RBAC: first page
First page of the user's documents through RLS (typical list view).read
rbac_select_count
RBAC: count visible
Count every document the user can see through RLS (worst case: touches every visible row).read
rbac_update_one
RBAC: update one row
Update one document in the user's home org by primary key (single-row write under RLS).write
write_assign_role
RBAC: assign a role
Admin write: assign a role to a new user in a random org.write
share_list_objects
Sharing: list visible ids
Sharing: every document the user can view through direct grants and nested teams.read
share_select_page
Sharing: first page
Sharing: first page through the two-branch RLS policy (org roles or shares).read
write_share
Sharing: share one document
Sharing admin write: share a random document with a random user (1 tuple, no lock).write
parents_proj_select_page
Parents, projection: first page
Parents, two-arm projection: first page through the policy from seed_parents.sql (shares by id, org roles and folder viewers projected onto folder_id; no hand-written org_id branch).read
parents_proj_select_count
Parents, projection: count visible
Parents, two-arm projection: count every document visible through the policy from seed_parents.sql (worst case: touches every visible row).read
parents_select_page
Parents, single branch: first page
Parents, single branch: first page through the policy from seed_parent_docs.sql (one engine call returns every visible document id: org roles and folder viewers through the document's parent link, plus shares).read
parents_select_count
Parents, single branch: count visible
Parents, single branch: count every document visible through the policy from seed_parent_docs.sql (worst case: touches every visible row).read
write_nest_team
Nesting: nest a team
Admin write where Design C pays most: nest the 1,000-member team md5('nest-src') (seeded by seed_sharing.sql) into a random team of the nested tree.write
nest_check
Nesting: check through a nested team
Read side of nesting: one check for a user who reaches a document only through a nested team.read
plain_list_objects
Conditions: list visible ids, unconditioned twin
Conditions, twin without: every document the user can view through `viewer` (shares, nested teams, parent folders), on the data of the conditions phase.read
cond_list_objects
Conditions: list visible ids through conditioned teams
Conditions: every document the user can view through `cviewer`, the conditioned copy of `viewer` (seed_conditions.sql: a condition on every team membership and team grant).read
cond_select_page
Conditions: first page through conditioned teams
Conditions: first page through the policies after seed_conditions.sql (the FGA policy on `cviewer`, org roles through seed_rbac.sql's RBAC policy).read
cond_nest_check
Conditions: check through a nested, conditioned team
Conditions: one Check through a nested, conditioned team.read
plain_list_users
Conditions: list users, unconditioned twin
Conditions, twin without: ListUsers (the admin API, fga_list_users) for the users who can view a document shared with a random team, through `viewer`.read
cond_list_users
Conditions: list users through conditioned teams
Conditions: ListUsers for the same documents as plain_list_users, through `cviewer` (every membership and grant on the way carries the condition).read
block_list_objects
Not monotone: list ids on viewer but not blocked
Not monotone: ListObjects on `can_view: viewer but not blocked` (seed_blocklist.sql), the set-based difference.read
block_or_mute_list_objects
Not monotone: list ids on viewer but not (blocked or muted)
Not monotone: ListObjects on `can_view_unmuted: viewer but not (blocked or muted)` (seed_blocklist.sql), a union under the subtract.read
intersect_list_objects
Not monotone: list ids on editor and viewer
Not monotone: ListObjects on `can_edit: editor and viewer` (seed_blocklist.sql), an intersection.read
row_select_page
Per row: first page, every row checked (Cedar only)
read