Auto-stop
Auto-stop is opt-in viaspec.autoStop.enabled=true. When enabled, the Firebolt Operator owns spec.replicas and drives it between two fixed levels. It scales the engine up to activeReplicas when a UTC schedule window is open or a wake-up is requested, and scales it down to idleReplicas (default 0, which fully stops the engine) after the engine sits idle for idleTimeout. This is an activity-gated on/off toggle, not proportional autoscaling. The engine runs at either idleReplicas or activeReplicas, never a value in between, and query volume never changes the count. Observed query activity only keeps the engine warm by resetting the idle clock. It never adds replicas.
Auto-stop reuses the same Prometheus signal the drain check consumes: firebolt_running_queries + firebolt_suspended_queries, summed across all running pods of the active generation. Sharing the signal keeps βthe engine is busyβ in exactly one place. A pod that the drain check would refuse to evict is the same pod auto-stop counts as activity.
Decision precedence
computeAutoStopDecision is a pure function over (spec, status, observation, now). Precedence, top-down:
- Disabled: If
spec.autoStopis unset orenabled=false, auto-stop emits no decision andspec.replicasis fully user-owned. - Wake requested: a gateway received a query for this engine within
DefaultAutoStopWakeTTL(5 minutes) while it had no ready endpoints. Replicas are scaled toactiveReplicas. ReasonWakeRequested. See Gateway wake-up protocol. - Schedule active:
nowfalls inside any window inspec.autoStop.schedule. Replicas are pinned atactiveReplicas. Schedule wins over both idle and stopped paths so an βalways-on during business hoursβ policy can wake a parked engine. - Stopped:
spec.replicas == 0, no fresh wake annotation, no schedule window active. No-op. - Scrape failed or activity observed: Refresh
status.lastActivityTime, do not scale. Scrape failures are grouped with activity intentionally because a broken probe must never look quiet enough to scale down. - Quiet >= idleTimeout, replicas > idleReplicas: Patch
spec.replicas = idleReplicasand stampstatus.lastScaledAt. - First quiet observation:
status.lastActivityTimeis anchored tonowso a fresh engine gets one fullidleTimeoutof grace.
Level-driven encoding
Scale events are encoded by patchingspec.replicas via the standard r.Update. The FireboltEngine watch fires. The next reconcile takes the normal blue-green path through creating, switching, draining, and cleaning. Auto-stop runs only in terminal phases (stable/stopped) so it cannot fight a rollout in progress.
Because scale-down only fires when firebolt_running_queries + firebolt_suspended_queries == 0 was just observed, the subsequent drain check on the old generation completes immediately. There is no wasted grace period. A separate βskip drain because auto-stop vouched for itβ path is unnecessary.
Configuration
Status fields
Known limitations
Auto-stop samples query activity once perpollInterval (default 1 minute), which leads to a few current edge cases:
- Brief queries can be missed. A query that runs for less than
pollIntervalmay not be counted as activity, so it does not reset the idle timer. A steady stream of very short queries can fail to keep an engine awake. - An engine can stop earlier than expected. An engine can scale down up to one
pollIntervalsooner than its configuredidleTimeout.
Gateway wake-up protocol
A FireboltEngine that auto-stop has scaled to zero replicas comes back to life when a query arrives for it. The query itself is the signal, and the query itself is what waits:- A query arrives for engine X. Before routing, Envoyβs Lua filter calls the wake agent on loopback. For a running engine the agent answers immediately from memory; for a stopped one it records demand and parks the request.
- The Firebolt Operator polls each gatewayβs demand endpoint every couple of seconds and sees fresh demand for an engine it knows is at zero replicas.
- Auto-stop scales the engine to
activeReplicas, reportingstatus.autoStopReason: WakeRequested. - The agentβs EndpointSlice watch sees the engineβs endpoints appear and releases the parked request. Envoy routes it, and the client gets an answer to the query it sent β not an error telling it to try again.
Why the gateway holds no credential
The gateway pod terminates untrusted traffic, so nothing in it may write to the Kubernetes API. The wake agentβs entire grant isget, list, watch on EndpointSlices, and demand travels outward on a metrics endpoint rather than inward as an API write. The Firebolt Operator, which already holds write permission, is the only component that scales anything.
The Envoy container holds no token at all. The pod sets automountServiceAccountToken: false and projects a short-lived, auto-rotating ServiceAccount token into the agent container alone β containers in a pod share the network namespace but not the mount namespace, so Envoyβs filesystem never contains a credential.
Why an in-pod agent rather than Envoy alone
The engineβs cluster Service is headless. At zero replicas it has no endpoints, so its name has no A records and the request fails at DNS resolution, which Envoy emits as a local reply. Retry policies act on upstream responses, so the gatewayβs own retries never see it and cannot ride out a cold start. Something has to hold the request until the engine exists, and the component that holds it is also the cheapest place to report the demand.Wake demand TTL
DefaultAutoStopWakeTTL (5 minutes) bounds how long an unrefreshed demand timestamp continues to trigger scale-up. Long enough to cover engine cold-start (image pull, blue-green creating phase) and short enough that an abandoned wake does not pin an engine indefinitely.
Demand is recorded as a timestamp, stamped when the request arrives, rather than as a count of requests currently waiting. A client that gives up before the Firebolt Operatorβs next poll still counts: it asked, and that is the reason to start the engine. This is also why a request shed under load still registers demand β the stamp happens before the capacity check.
What a query to a stopped engine costs
The first query pays the engineβs cold start, typically tens of seconds. Set your clientβs timeout above that or it will give up while the engine is still coming up. Subsequent queries are unaffected. Requests are held, not buffered in full: the agent reads nothing but the engine name, and Envoyβs flow control stalls the clientβs upload rather than accumulating its body. The number of simultaneously held requests is capped from the gatewayβs memory budget; past the cap, requests get503 with Retry-After while the wake still proceeds.
Formal model
The demand path and the agentβs hold path are both specified in TLA+ and model-checked in CI:formal/EngineWake.tla covers the agentβs stamp, the Firebolt Operatorβs cached copy of it and the auto-stop decision that reads it, and formal/WakeAgentHold.tla covers the readiness waiter that releases a held query. Between them they establish that an engine only ever scales up on demand that is still within the TTL at the moment of the decision, that demand for a running engine never changes its replica count, and that a held query is always releasable when endpoints appear. See Formal verification for what each specification proves and for the one lost-wakeup case the design accepts.
Wake and deliberately stopped engines
Auto-stop cannot distinguish an engine it parked from one you set toreplicas: 0 by hand β both are engines at zero replicas β so a query through the gateway will start either. To keep an engine down, set spec.autoStop.enabled: false alongside replicas: 0; auto-stop then reports Disabled and leaves the engine alone.
Reaching a stopped engine
Wake only fires for traffic through the gateway. Clients that resolve an engineβs headless Service themselves and connect directly get no answer from a stopped engine and nothing wakes it, because a stopped engine has no endpoints for them to resolve in the first place. Route through the gateway if you rely on auto-stop.Gateway RBAC
Each FireboltInstance provisions per-gateway RBAC alongside the gateway Deployment:
The ClusterRole is shipped by the Helm chart rather than created by the Firebolt Operator, which is what keeps
roles: create/update/patch out of the Firebolt Operatorβs own RBAC.
Disabling wake
SetwakeAgent.enabled: false in the chart. The sidecar is omitted and the RoleBinding is not created; a query for a stopped engine gets a 503 instead of waiting, and nothing wakes the engine. Query routing to running engines is unaffected.