Running Spatial Dashboards on Spot Nodes
A reclaim is a node drain compressed into the warning window, so spot is survivable exactly when the drain hooks already work. Taint the pool, prefer it with weighted affinity, spread across capacity types, and keep enough on-demand to absorb a correlated reclaim.
Jump to heading Why this matters
Spot and preemptible instances are sixty to ninety percent cheaper than on-demand, and a spatial dashboard is an expensive thing to run — the memory required for resident GeoDataFrames pushes it onto larger instance types than a typical web service. The saving is real and the temptation to take it wholesale is strong.
What makes it risky here is not the interruption itself but the session model. A stateless handler that is interrupted mid-request is retried by the load balancer and nobody notices. A dashboard holding WebSocket sessions has no such mechanism: the socket closes, the map goes blank, and the analyst reconnects to a replica that has none of their state. The interruption is not a request failure; it is a visible one, for everybody on that node, at the same moment.
Jump to heading Prerequisites
- The graceful-termination setup from Kubernetes autoscaling & orchestration: a
preStophook, a sensibleterminationGracePeriodSeconds, and aPodDisruptionBudget. These are prerequisites rather than refinements — without them spot is simply worse. - A reconnect path that rehydrates from session state or from a shared store, so a reconnected analyst lands where they were.
- Node pools labelled by capacity type, which every managed Kubernetes offering does automatically.
Jump to heading Step-by-step solution
Jump to heading Step 1 — Make on-demand the default
# On the spot pool, so nothing lands there by accident.
taints:
- key: capacity
value: spot
effect: NoSchedule
Tainting rather than merely labelling is the important choice. A label expresses a fact and a taint expresses a policy: with the taint in place, a workload ends up on spot only because somebody wrote a toleration for it, which means the decision is visible in the manifest of every service that took it.
Jump to heading Step 2 — Prefer spot, do not require it
tolerations:
- key: capacity
operator: Equal
value: spot
effect: NoSchedule
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: capacity
operator: In
values: ["spot"]
preferred rather than required is what keeps a cost optimisation from becoming an outage. Spot capacity is not always available; a required affinity leaves pods Pending until it is, which means the dashboard is down at exactly the moment the market is tight. Preferred lets the scheduler pay for on-demand instead, which is the correct answer to “spot is unavailable”.
Jump to heading Step 3 — Stop the scheduler from concentrating everything
topologySpreadConstraints:
- maxSkew: 1
topologyKey: capacity
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels: {app: spatial-dashboard}
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels: {app: spatial-dashboard}
Without the first constraint, a weighted preference for spot is satisfied perfectly well by putting every replica on spot — which recreates the all-spot failure inside what the manifest describes as a mixed fleet. The second spreads across zones, because spot reclaims frequently correlate within a zone as well as within a pool.
Jump to heading Step 4 — Size the split against a correlated failure
The number to design against is not the probability of a reclaim but what happens during one. Every session on the reclaimed capacity reconnects at once, and the surviving capacity has to absorb that burst plus whatever load it was already carrying — which is the same burst-headroom problem covered in tuning HPA for bursty sessions, arriving without warning rather than at nine in the morning.
Jump to heading Step 5 — Handle the reclaim notice
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 20"]
terminationGracePeriodSeconds: 25 # inside the shortest reclaim notice
Twenty-five seconds looks miserly beside the ninety a rolling update deserves, and it is deliberate: a grace period longer than the reclaim notice is not honoured, because the machine is removed regardless. Size this one against the shortest notice your provider gives and keep the longer period for ordinary drains by using a separate deployment for the on-demand half if the difference matters.
Jump to heading Verification
# 1. Nothing schedules onto spot without a toleration.
kubectl get pods -o wide --field-selector spec.nodeName!="" \
-o custom-columns='POD:.metadata.name,NODE:.spec.nodeName' | head
# 2. Replicas are actually spread across capacity types.
kubectl get pods -l app=spatial-dashboard -o json \
| jq -r '.items[].spec.nodeName' | xargs -I{} kubectl get node {} \
-o jsonpath='{.metadata.labels.capacity}{"\n"}' | sort | uniq -c
# 3. Drain a spot node and watch the sessions.
kubectl drain <spot-node> --ignore-daemonsets --delete-emptydir-data
The third check is the one that matters and the one teams skip. Draining a node deliberately, during a working afternoon, with somebody watching a dashboard session, is the only way to find out whether the reconnect path actually rehydrates — and it is far better to discover it does not while you are holding the keyboard.
Jump to heading Edge cases and gotchas
- Cluster autoscaler and spot together. A scale-up that requests spot capacity which is unavailable can leave pods pending while the autoscaler retries. Configure the node group to fall back to on-demand, or run a small always-on-demand pool that can absorb the difference.
- Sticky sessions plus spot. A cookie pins an analyst to a pod that may vanish. That is fine provided the reconnect issues a new cookie and rehydrates — which is the behaviour described in sticky sessions on Kubernetes, and it needs testing under reclaim rather than only under rollout.
- The reclaim handler is not a shutdown hook. Node-termination handlers cordon and drain on the notice; they do not extend it. Treat them as the thing that starts the drain promptly, not as extra time.
- Stateful sidecars. A pod with a local cache warmed over an hour loses it on every reclaim. Move that cache to the shared tier, or keep those pods off spot entirely.
Jump to heading FAQ
Is spot worth it for a small fleet?
Often not. Below about four replicas the arithmetic of a correlated reclaim is unforgiving — losing half of four is losing two, and the survivors cannot absorb the reconnections. The saving also matters less in absolute terms. Spot pays off on fleets large enough that a partial reclaim is a degradation rather than an outage.
Should the cache layer run on spot?
No. A shared cache is the thing that makes a reconnected session cheap, so putting it on interruptible capacity means the reclaim takes both the sessions and the mechanism for recovering from it. Keep the shared tier on on-demand and let the application replicas take the risk.
How do I know a reclaim happened rather than a crash?
The node events record it, and the pod’s termination reason differs from an OOM kill or a liveness restart. Log the node name with every session start, as part of the structured logging schema, and a reclaim becomes visible as a cluster of sessions ending together on one node — which is also how you distinguish it from the memory failure it otherwise resembles.
Back to Kubernetes Autoscaling & Orchestration.