Kubernetes guide
Stateless Services on Kubernetes: Design, Deploy, and Scale Safely
A stateless service can be replaced at any time without losing the durable context needed to complete correct work. Design for that property before adding replicas.
Kubernetes makes replacement normal: pods are restarted, rescheduled, upgraded, and scaled. A service that depends on one pod's memory, local disk, or network identity will eventually fail in ways that are difficult to reproduce. Stateless design turns those lifecycle events into routine operations.
Stateless does not mean a service has no state. It means durable business state lives in a suitable external system, and any instance can process a request using that state. A pod may cache data or hold an in-flight connection, but correctness must not depend on that transient data surviving.
Identify the state before moving it
List every item that a process keeps beyond a single request: login sessions, upload progress, job ownership, rate-limit counters, idempotency keys, generated files, WebSocket routing data, and cache entries. For each item, decide whether it is durable, reconstructable, or disposable.
- Store durable records in a database, object store, or queue with an explicit retention and consistency model.
- Store shared ephemeral state, such as a distributed session or lease, in a purpose-built shared store when it is truly needed.
- Keep reconstructable caches local only if a cold cache does not harm correctness or overload the origin.
- Treat pod-local filesystems as temporary unless a workload deliberately uses a persistent volume and its operational constraints.
Avoid using sticky sessions to hide statefulness. Affinity can improve cache locality, but it does not protect against a pod restart, rollout, or zone failure. If a user action must reach the same instance to succeed, the system has an availability and scaling constraint that should be made explicit.
Build a deployable stateless service
Start with an immutable image and a small, explicit runtime contract. The process should bind to the configured port, expose health endpoints, read configuration from the deployment environment, and emit logs to standard output or a collector. It should not expect a particular node, public IP, or writable application directory.
Use a Deployment for independently replaceable HTTP or gRPC services. Give it resource requests based on measured steady-state demand and limits chosen to prevent one workload from harming others. A CPU limit can cause throttling and long tail latency; measure before applying it universally. Memory limits are essential protection, but an out-of-memory restart is not a capacity plan.
Readiness, liveness, and startup probes
Configure probes to reflect distinct questions:
- Startup: has the application completed its bounded initialization?
- Readiness: can this instance accept its assigned traffic now?
- Liveness: is the process unrecoverably stuck and worth restarting?
Keep probe endpoints inexpensive and independent of user authentication. Do not make them perform expensive queries or call every optional downstream dependency. During an outage, a dependency-sensitive liveness check can cause a restart storm; readiness can remove an instance from traffic when it cannot safely serve, while circuit breakers and fallbacks protect dependency calls.
A Pod Disruption Budget is not a guarantee of availability
A Pod Disruption Budget limits voluntary disruptions, subject to cluster conditions. It does not stop application crashes, capacity shortages, node failures, or a bad rollout. Design replicas, topology spread, and graceful shutdown for the failure modes you need to survive.
Drain requests and work correctly
Kubernetes sends SIGTERM before it removes a container after its termination grace period. The application must cooperate. On termination, fail readiness immediately, stop accepting new connections, and allow in-flight requests a bounded time to finish. Align load-balancer draining, pre-stop behavior, application shutdown timeout, and terminationGracePeriodSeconds so they do not contradict one another.
For asynchronous workers, use acknowledged queues and idempotent handlers. A worker can be terminated after receiving a message but before recording its result. Make duplicate delivery safe with a durable idempotency key or transactional outbox pattern. Extend a message lease only while the worker is healthy, and route repeatedly failing work to a dead-letter process with an owner.
Long-lived connections deserve a separate plan. WebSockets, streaming responses, and large uploads cannot be drained like a 50 ms API call. Set maximum connection age, communicate reconnect behavior to clients, and roll out gradually. For global audiences, consider where connection termination and application state live; an edge connection layer may reduce round-trip latency, but the origin protocol still needs safe reconnect and state recovery behavior.
Scale without moving the bottleneck
Horizontal Pod Autoscaling can add replicas based on CPU, memory, or custom metrics. Pick a signal related to useful capacity. Queue depth, queue age, active requests, or concurrency may be more meaningful than CPU for an I/O-bound service. Validate the complete loop: metric freshness, scale-up delay, node capacity, image pull time, application warm-up, and downstream connection limits.
Set a concurrency limit at the application or proxy layer. Otherwise, more traffic can create unbounded goroutines, threads, promises, or database calls inside each replica. Backpressure is a correctness feature: queue, shed low-priority work, return a retryable response, or serve a cached/degraded result before the service reaches failure.
Capacity planning must include shared resources. If each new pod opens twenty database connections, an autoscaler that grows from ten to one hundred pods requests two thousand connections. Cap per-pod pools, use a database-aware proxy where appropriate, and alert on pool wait time, not only pod CPU.
Keep latency visible from edge to dependency
Measure user-facing latency as well as cluster metrics. A healthy pod can still produce a slow experience because of DNS, TLS, cache misses, regional routing, a serial API chain, or an overloaded database. Propagate W3C trace context from the ingress or edge to the service and dependency clients, and attach route, release version, region, and outcome to telemetry.
Track p50, p95, and p99 latency, error rate, saturation, queue age, connection-pool wait, and cache hit ratio. Correlate them with rollout and autoscaling events. The OpenTelemetry Kubernetes guidance is a useful starting point for collecting signals without making logs the only source of truth.
For cacheable public traffic, use correct Cache-Control and cache-key rules at the edge. Keep authenticated, personalized, and mutation routes explicit. Edge caching reduces origin load only when invalidation, variation, and authorization semantics are correct; it should never cache a response merely because the origin was slow.
Test the replacement contract
Before calling a service stateless, run controlled tests:
- Delete one pod during live-like traffic and confirm requests drain or retry safely.
- Roll out a new version while holding long-running requests and queue work.
- Scale replicas up and down while watching database connections and tail latency.
- Simulate a dependency timeout and verify deadlines, circuit breakers, and fallback behavior.
- Remove a node or zone in a non-production environment and confirm topology and capacity assumptions.
Also test a cold start. An image that takes minutes to pull or initialize cannot respond quickly to a traffic spike, even if the autoscaler chooses the right replica count.
Authoritative references
- The Twelve-Factor App: Processes
- Kubernetes: Deployments
- Kubernetes: Pod lifecycle and termination
- Kubernetes: Horizontal Pod Autoscaling
- OpenTelemetry on Kubernetes
Design a delivery path that degrades predictably
Optimi can help assess edge, origin, caching, and traffic-routing architecture around your Kubernetes-hosted services.
Review delivery resilience