Config push vs poll: everyday use
PR #4320 (config_delivery_mode = "push") at 88f81d812 · one gateway, Docker driver, Postgres, 250 sandboxes, plus a 100-sandbox kind cluster with 2–3 gateway replicas · October 2026
Every change reached every targeted sandbox in both modes, and the in-sandbox checks matched the new configuration.
What changed since the last report
- The code. The previous report measured #4320 at
45fc53a10. Since then the delivery was simplified: one update carries the sandbox configuration and the provider environment it names, each session has one update in flight on its ordered stream, there are no priority lanes, and results are written as they arrive and only when they change. Supervisors also start by polling again, as on main, and switch to push once their session is accepted. - The scenarios. The previous run stressed bursts: one command per sandbox for all 250 at once, and 20 global changes in 2 seconds. This run models everyday use: one change at a time to different sandboxes, steady background activity, an occasional fleet-wide change, sandboxes stopping, starting and being replaced, a gateway restart, and gateway replicas coming and going.
How we tested
- One gateway built from the PR, with the Docker compute driver and Postgres 16, on a 20-core arm64 host. Supervisor, sandbox and gateway images were built from the same commit. Poll mode uses the default 10-second interval; push reconciles every 30 seconds.
- A fleet of 250 sandboxes per mode, recreated for each mode. With 250 sandboxes the host stays almost fully busy (load average 200–300), so both modes run on a saturated machine.
- Latency is per sandbox: from the moment the CLI command was issued to the supervisor's own log line confirming the change was applied. Both modes apply through the same supervisor code, so the measurement is identical.
- One-at-a-time scenarios change 20 different sandboxes, 3 seconds apart, so every number is one change, not a burst. Scenario groups use different sandboxes.
- Inside the sandboxes we checked that allow and deny rules flipped, that rotated bearer tokens reached the upstream, that provider placeholders appeared and disappeared, that a stopped sandbox started with the policy edited while it was stopped, and that replacement sandboxes enforced their policy. Every check passed in both modes.
- Other work shares this host, so before every scenario the harness waited until no other build or test was running.
Apply latency, p99
All change scenarios
| Change | Scope | Push p50 | Push p99 | Poll p50 | Poll p99 | p99 speed-up | Converged push · poll |
|---|---|---|---|---|---|---|---|
| One change at a time | |||||||
| Policy edit | 20 sandboxes, one edit each, 3 s apart | 60 ms | 99 ms | 5.6 s | 9.6 s | 96.7× | 20/20 · 20/20 |
| Sandbox setting | 20 sandboxes | 49 ms | 117 ms | 5.0 s | 9.0 s | 76.8× | 20/20 · 20/20 |
| Draft rule approval | 20 sandboxes | 74 ms | 135 ms | 6.4 s | 9.9 s | 73.7× | 20/20 · 20/20 |
| Provider attach | 20 sandboxes | 71 ms | 114 ms | 5.3 s | 9.6 s | 84.5× | 20/20 · 20/20 |
| Provider detach | 20 sandboxes | 58 ms | 98 ms | 5.7 s | 9.7 s | 99.8× | 20/20 · 20/20 |
| Steady activity | |||||||
| Mixed changes at 1/s 70% policy edits, 30% settings | 160 changes, each to a different sandbox | 54 ms | 125 ms | 5.6 s | 9.8 s | 78.2× | 160/160 · 160/160 |
| Occasional fleet-wide change | |||||||
| Global policy change | every sandbox | 674 ms | 1.5 s | 6.5 s | 10 s | 6.7× | 250/250 · 250/250 |
| Global policy revert | every sandbox | 903 ms | 1.5 s | 4.2 s | 7.8 s | 5.1× | 250/250 · 250/250 |
| Credential rotation median of 3 rotations | provider every sandbox attaches | 747 ms | 1.6 s | 3.0 s | 7.7 s | 4.7× | 250/250 · 250/250 |
| Sandbox lifecycle | |||||||
| Edit after a graceful gateway restart | 20 sandboxes | 53 ms | 95 ms | 4.9 s | 9.9 s | 104.1× | 20/20 · 20/20 |
| Global change after a graceful gateway restart | every sandbox | 1.1 s | 1.6 s | 4.9 s | 9.9 s | 6.1× | 250/250 · 250/250 |
| Fault | |||||||
| Catch-up after a 20 s supervisor stall edited while stalled | 10 sandboxes, measured from resume | 335 ms | 862 ms | 606 ms | 10 s | 12.0× | 10/10 · 10/10 |
Sandbox lifecycle
| Push | Push check | Poll | Poll check | |
|---|---|---|---|---|
| Start a stopped sandbox, with a policy edit made while stopped start to Ready, p50 / max | 3.2 s / 4.7 s | 10/10 | 4.0 s / 5.4 s | 10/10 |
| Replace a sandbox: delete one, create its replacement create to Ready, p50 / max | 3.1 s / 5.1 s | 10/10 | 3.3 s / 6.0 s | 10/10 |
| Graceful gateway restart every sandbox Ready / every session back, after the gateway started | 316 s / 316 s | 250/250 | 319 s / 319 s | 250/250 |
Starting, replacing and restarting take the same time in both modes, because supervisors start by polling in both. Push only changes what happens once the session is up. A graceful gateway restart on Docker stops every sandbox and resumes them one at a time, so the fleet is back after about five minutes in both modes. Changes made right after the restart arrive as fast as before it.
Gateway replicas coming and going (Kubernetes)
A kind cluster on the same host with 100 sandboxes and a gateway Deployment that starts with 2 replicas. Changes go through a NodePort, so most of them land on a replica that does not own the sandbox's session and reach the owner through peer notification. Scaling out does not move established sessions, so a new replica starts with none and forwards the changes it receives. Sessions on a replica that stops or is lost reconnect to the others.
| Change | Push p50 | Push p99 | Poll p50 | Poll p99 | p99 speed-up | Converged push · poll |
|---|---|---|---|---|---|---|
| Policy edit, one at a time 20 sandboxes, through a random replica | 23 ms | 31 ms | 6.1 s | 8.9 s | 288.0× | 20/20 · 20/20 |
| Steady activity at 1/s 40 changes | 20 ms | 29 ms | 3.5 s | 9.9 s | 335.8× | 40/40 · 40/40 |
| Global policy change every sandbox | 58 ms | 141 ms | 5.1 s | 9.8 s | 69.5× | 100/100 · 100/100 |
| Credential rotation median of 3 | 53 ms | 121 ms | 5.2 s | 9.4 s | 77.5× | 100/100 · 100/100 |
| Edits while scaling 2 → 3 replicas one edit per second, from the scale-up until 30 s after it (at least 60 s) | 19 ms | 31 ms | 6.0 s | 9.8 s | 315.6× | 60/60 · 60/60 |
| Edits during a rolling restart one edit per second, from the restart until 30 s after it (at least 60 s) · CLI errors 2 / 1 | 21 ms | 30 ms | 4.7 s | 9.6 s | 319.0× | 58/58 · 59/59 |
| Edits after losing one replica 20 sandboxes, right after the force-delete | 27 ms | 40 ms | 6.6 s | 9.6 s | 241.0× | 20/20 · 20/20 |
| Edits while scaling 3 → 2 replicas one edit per second, from the scale-down until 30 s after it (at least 60 s) | 20 ms | 28 ms | 4.8 s | 9.5 s | 335.7× | 60/60 · 60/60 |
| Global change after scaling 2 → 3 every sandbox | 58 ms | 146 ms | 5.5 s | 9.8 s | 67.3× | 100/100 · 100/100 |
| Global change after a rolling restart every sandbox | 37 ms | 118 ms | 4.7 s | 10.0 s | 84.8× | 100/100 · 100/100 |
| Global change after losing one replica every sandbox | 58 ms | 135 ms | 4.7 s | 10.0 s | 73.5× | 100/100 · 100/100 |
| Global change after scaling 3 → 2 every sandbox | 59 ms | 162 ms | 5.6 s | 9.9 s | 61.0× | 100/100 · 100/100 |
| Replica change | Push: took | Push: sessions per replica after | Poll: took | Poll: sessions per replica after |
|---|---|---|---|---|
| Scale 2 → 3 | 2.8 s | 58 / 42 / 0 | 2.7 s | 55 / 45 / 0 |
| Rolling restart | 9.1 s | 44 / 31 / 25 | 9.1 s | 44 / 42 / 14 |
| Scale 3 → 2 | 115 ms | 52 / 48 | 97 ms | 59 / 41 |
| Force-delete one replica | 44 sessions moved in 611 ms | 44 sessions moved in 582 ms | ||
Idle cost, 250 sandboxes
| Gateway CPU | Gateway RSS | gRPC requests/s | Config work | |
|---|---|---|---|---|
| Push | 18.0% | 1262 MB | 50 | 8.3 reconcile builds/s |
| Poll | 21.7% | 1257 MB | 75 | 25 config polls/s |
Observations
- Fleet-wide changes are the slow push case. A global policy change or a credential rotation rebuilds every session's update, 20 builds at a time on one gateway, and resolves credentials again, so it reaches all 250 sandboxes in 1.4–1.7 s at p99. On Kubernetes, with 100 sandboxes spread over replicas that build in parallel, the same changes finish in under 200 ms.
- A frozen supervisor takes its workload down after 40 seconds. The sandbox runtime freezes the workload once its supervisor misses keepalives for 10 seconds and terminates it 30 seconds later. This is the same in both modes. The first attempt at the stall scenario used 40 seconds and lost one sandbox this way; the scenario now stalls for 20 seconds.
- CLI requests during a rolling restart. On Kubernetes, 1 of 60 CLI requests in poll mode and 2 of 60 in push mode failed with an HTTP/2 keep-alive timeout while the gateway Deployment was rolling. The request reached a pod that was shutting down and never got an answer. Scaling up, scaling down and losing a replica caused no failures. This is the CLI's own API path, which this PR does not change, so it is a separate follow-up: drain connections on shutdown, or retry safe requests.
- Idle request rate. The remaining 50 requests per second in both modes are provider readiness reports, one every 5 seconds per sandbox. Moving them onto the supervisor session is a follow-up.
Not covered here
- Several hosts. The Kubernetes run uses one kind node on the same host, so its fleet is 100 sandboxes. Images were already on the node, so replicas started in seconds; pulling images would make scale-ups and rolling restarts take longer.
- Interceptor provider-profile sources, time-based credential refresh, and Vault, which need fixtures we have not built.