Every permission set and every position an environment declares costs exactly 4 sequential database round trips on every kernel boot, and 2 of those 4 are an UPDATE that fires even when nothing changed.
Measured
Real per-environment kernel build (ArtifactKernelFactory.create(), hosted capability slate, real TursoDriver in remote transport with every @libsql/client call counted), measuring a REBUILD — tables present, rows already seeded, nothing to change:
permission sets round trips positions round trips
0 242 0 242
5 262 5 262
10 282 10 282
20 322 20 322
40 402 40 402
Least squares: slope exactly 4.0000, R² = 1.000000 on both axes.
Diffing the per-statement round-trip histogram between 0 and 40 of each names the four:
per permission set per position
+2 SELECT * FROM sys_permission_set …LIMIT +2 SELECT * FROM sys_position … LIMIT
+1 UPDATE sys_permission_set SET … +1 UPDATE sys_position SET …
+1 SELECT * FROM sys_permission_set … +1 SELECT * FROM sys_position …
For contrast, on the same rig objects, views and artifact seed records add 0.00 round trips each (0 → 400 objects all boot in 242): schema sync is already batched through TursoDriver.supports.batchSchemaSync. Identity content is the one content axis that is not.
Why it costs what it costs
bootstrapDeclaredPermissionSets (packages/plugins/plugin-security/src/bootstrap-declared-permissions.ts:166) awaits upsertPackagePermissionSet once per set inside a for loop; the position binder has the same shape. On a local file database the loop is invisible. On a remote libsql/Turso database — every hosted environment — each leg is its own HTTP request, and the legs are awaited one after another.
The same rig measures how much that matters: injecting a synthetic per-call latency grows the whole bootstrap step at 171.7 ms per ms of RTT (R² = 0.998), so ~172 of the 242 baseline round trips sit on the critical path. At that rate 40 permission sets plus 40 positions add 320 round trips — more than doubling a boot that a request will only wait 20 s for (objectstack-ai/cloud#1555).
Direction
Both loops are read-then-write per item over a set known in full before the loop starts:
- Replace the per-item existence
SELECT with ONE find({ name: { $in: [...] } }) before the loop. - Skip the
UPDATE when the stored row already equals what would be written — an idempotent reconcile that writes unconditionally is paying two round trips per item forever to change nothing.
That takes the steady-state rebuild cost of both loops to O(1) round trips, leaving writes only where something genuinely drifted.
Reproducing
objectstack-ai/cloud:
node scripts/dev-local/bootstrap-curve.mjs --hosted --permission-sets 0,5,10,20,40
node scripts/dev-local/bootstrap-curve.mjs --hosted --positions 0,5,10,20,40
node scripts/dev-local/bootstrap-curve.mjs --hosted --permission-sets 40 --positions 40 --sql-histogram
Refs objectstack-ai/cloud#1555
Every permission set and every position an environment declares costs exactly 4 sequential database round trips on every kernel boot, and 2 of those 4 are an UPDATE that fires even when nothing changed.
Measured
Real per-environment kernel build (
ArtifactKernelFactory.create(), hosted capability slate, realTursoDriverin remote transport with every@libsql/clientcall counted), measuring a REBUILD — tables present, rows already seeded, nothing to change:Least squares: slope exactly 4.0000, R² = 1.000000 on both axes.
Diffing the per-statement round-trip histogram between 0 and 40 of each names the four:
For contrast, on the same rig objects, views and artifact seed records add 0.00 round trips each (0 → 400 objects all boot in 242): schema sync is already batched through
TursoDriver.supports.batchSchemaSync. Identity content is the one content axis that is not.Why it costs what it costs
bootstrapDeclaredPermissionSets(packages/plugins/plugin-security/src/bootstrap-declared-permissions.ts:166) awaitsupsertPackagePermissionSetonce per set inside aforloop; the position binder has the same shape. On a local file database the loop is invisible. On a remote libsql/Turso database — every hosted environment — each leg is its own HTTP request, and the legs are awaited one after another.The same rig measures how much that matters: injecting a synthetic per-call latency grows the whole
bootstrapstep at 171.7 ms per ms of RTT (R² = 0.998), so ~172 of the 242 baseline round trips sit on the critical path. At that rate 40 permission sets plus 40 positions add 320 round trips — more than doubling a boot that a request will only wait 20 s for (objectstack-ai/cloud#1555).Direction
Both loops are read-then-write per item over a set known in full before the loop starts:
SELECTwith ONEfind({ name: { $in: [...] } })before the loop.UPDATEwhen the stored row already equals what would be written — an idempotent reconcile that writes unconditionally is paying two round trips per item forever to change nothing.That takes the steady-state rebuild cost of both loops to O(1) round trips, leaving writes only where something genuinely drifted.
Reproducing
objectstack-ai/cloud:Refs objectstack-ai/cloud#1555