Running a Backend on Kubernetes
What the application must do to be a well-behaved workload: honest probes, a termination sequence that races endpoint removal, resource requests that match reality, and no local disk.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind has a problem.
What does my application code have to get right for Kubernetes to run it well?
The platform team runs a cluster. Our service has to deploy onto it, scale with it, and survive nodes being drained without anyone noticing.
Write a Deployment manifest with the image and a port, add a readiness probe pointing at /health, set some resource limits, and let the cluster handle the rest.
/health returns 200 as long as the process is running, so a pod with a broken database connection is declared ready and receives traffic immediately (Health Checks: Startup, Readiness, Liveness).
/healthreturns 200 as long as the process is running, so a pod with a broken database connection is declared ready and receives traffic immediately (Health Checks: Startup, Readiness, Liveness).- The same endpoint is used for liveness, so when the database is slow the liveness probe fails, the pod is killed and restarted, and the restart makes the database problem worse (Cascading Failure).
- The pod receives SIGTERM and exits immediately, while the endpoint removal that stops traffic is still propagating — every rollout drops requests (Graceful Shutdown).
- A memory limit is set below the runtime's actual working set, so the pod is OOMKilled under load with no application error and no stack trace.
- A CPU limit of a few hundred millicores throttles the process during bursts; latency rises while CPU utilisation reads far below the limit, and every dashboard says the service is idle.
- The service writes uploads to the container filesystem, and the files disappear the next time the pod is rescheduled (File Uploads Through the Backend).
- Every replica runs the scheduled job, so a nightly report is generated once per pod (Scheduled Jobs).
What is actually happening
- Kubernetes is a control loop over declared desired state. You declare replicas, images, probes and resource requests; controllers work continuously to make the cluster match. Nothing about it is a request-response deploy.
- A readiness probe controls membership of the Service endpoints — whether traffic is routed to this pod. A liveness probe controls whether the kubelet restarts the container. They answer different questions and conflating them turns a dependency blip into a restart storm (Liveness vs Readiness in Cloud & Infrastructure).
- On pod deletion, two things happen concurrently: the pod is removed from endpoints (propagating asynchronously to every kube-proxy and ingress controller), and the container receives SIGTERM. There is no ordering guarantee, so traffic can arrive after SIGTERM — which is exactly why the drain delay exists.
terminationGracePeriodSecondsis the budget between SIGTERM and SIGKILL. Your drain must complete inside it, and the manifest number and the application timeout must be kept consistent.- Requests drive scheduling and are what the scheduler reserves; limits are enforced at runtime — memory by the OOM killer, CPU by cgroup throttling. Requests too low means eviction and noisy-neighbour effects; limits too low means kills and throttling (Requests vs Limits: Two Numbers That Do Different Jobs in Cloud & Infrastructure).
- Pods are cattle in the strict sense: rescheduled on node pressure, drained on node upgrade, evicted on resource pressure, preempted by higher priority. Termination is routine, not exceptional.
- Horizontal Pod Autoscaling adjusts replica count from a metric. It is a controller with a lag: measurement interval, plus scheduling, plus image pull, plus your startup time (Autoscaling a Backend).
The manifest is application configuration
GOMEMLIMIT instead. The requirement — tell the runtime its ceiling — is universal.Four fields in a Deployment decide most of how the service behaves in production, and all four encode decisions that belong to the application engineer: what "ready" means, what "alive" means, how long shutdown may take, and how much CPU and memory the process actually needs.
Read the manifest below as a contract between your process and the cluster. Every value in it corresponds to something the code must actually implement — a readiness endpoint that can fail, a SIGTERM handler that drains, a heap that fits inside the limit.
1spec:2 replicas: 63 strategy:4 rollingUpdate:5 maxUnavailable: 0 # never dip below capacity6 maxSurge: 27 template:8 spec:9 # must exceed drain delay + drain deadline in the app10 terminationGracePeriodSeconds: 4511 containers:12 - name: api13 image: registry/api@sha256:... # digest, not a tag14 15 # "am I wedged?" — no dependency calls, ever16 livenessProbe:17 httpGet: { path: /livez, port: 8080 }18 periodSeconds: 1019 failureThreshold: 320 21 # "should I get traffic?" — deps + draining flag22 readinessProbe:23 httpGet: { path: /readyz, port: 8080 }24 periodSeconds: 2 # short: drain must be noticed fast25 failureThreshold: 226 27 # protects a slow start from the liveness probe28 startupProbe:29 httpGet: { path: /livez, port: 8080 }30 periodSeconds: 531 failureThreshold: 3032 33 resources:34 requests: { cpu: "500m", memory: "512Mi" }35 limits: { memory: "512Mi" } # equal to request36 # no CPU limit: requests give a share; a tight limit37 # adds throttling that looks like slow code38 39 env:40 - name: NODE_OPTIONS # runtime must know the limit41 value: "--max-old-space-size=384"42 43 securityContext:44 runAsNonRoot: true45 readOnlyRootFilesystem: true46 allowPrivilegeEscalation: falseThe two most consequential lines are the ones that look like tuning. terminationGracePeriodSeconds must be larger than the application's own drain budget, or the careful shutdown never finishes. And --max-old-space-size below the memory limit is what stops a runtime that reads host memory from planning a heap the cgroup will not allow — an OOMKill with no application error.
Probes: two questions, two consequences
The single most damaging Kubernetes configuration mistake made by application teams is one endpoint serving both probes. Because the probes trigger different actions, a shared implementation forces a choice that is wrong in one direction or the other.
If the shared endpoint checks the database, a database slowdown restarts every pod, and the restarts hit the database with reconnection and cache-warming load, converting a degradation into an outage. If the shared endpoint checks nothing, unready pods receive traffic and return errors. The only correct answer is two endpoints with different meanings.
app.get('/health', async (_req, res) => {
await db.query('SELECT 1') // dependency check
await redis.ping()
res.send('ok')
})
// livenessProbe: /health
// readinessProbe: /health
//
// DB gets slow ->
// readiness fails (correct: stop traffic)
// liveness fails (wrong: kill every pod)
// -> full restart of the fleet, into a slow DB// LIVENESS: "is this process wedged?"
// No I/O. No dependencies. Cheap and local.
app.get('/livez', (_req, res) => res.send('ok'))
// READINESS: "should I receive traffic?"
app.get('/readyz', async (_req, res) => {
if (draining) return res.status(503).send('draining')
if (!configValidated) return res.status(503).send('starting')
try {
await db.query('SELECT 1') // cached for ~1s to avoid
res.send('ok') // probing the DB every 2s
// from every replica
} catch {
res.status(503).send('db unavailable')
}
})Readiness failing removes the pod from the load balancer and is reversible in seconds. Liveness failing destroys the process and its warm state. Tying the destructive action to a dependency you do not control means an external problem becomes a self-inflicted one — and the restarts add load to the very dependency that was struggling.
The termination race, in the cluster's own terms
Kubernetes does not stop traffic and then tell your process to exit. It does both at once, and one of them is eventually consistent across every node in the cluster. Endpoint removal has to propagate to every kube-proxy and every ingress controller, and until it has, requests keep arriving at a pod that has already been told to shut down.
This is why the drain delay is not a workaround — it is the application's side of a protocol whose other side is asynchronous by design. The pod must keep serving normally for long enough that the removal has propagated, and only then close its listener.
How to build it
Most important first.
- Split the probes. Readiness checks the dependencies you cannot serve without and returns failure while draining. Liveness checks only that the process is not wedged — no dependency calls at all.
- Implement the shutdown sequence with a drain delay long enough to outlast endpoint propagation, and set
terminationGracePeriodSecondsabove your drain deadline (Graceful Shutdown). - Use a
startupProbefor slow-starting services so the liveness probe does not kill a process that is merely warming up. - Set memory requests and limits equal for predictable behaviour, and derive them from measured working set rather than guesses; make the language runtime aware of the limit so it does not size its heap from the host.
- Be cautious with CPU limits. CPU requests give you a share; a tight limit adds throttling that shows as latency with no visible saturation. Many teams set CPU requests and no CPU limit deliberately.
- Take configuration from environment variables and mounted files, validate it at startup, and fail fast with a clear message rather than at first request (Validate at Startup, Fail Loudly).
- Log to stdout as structured JSON; the cluster collects it. Do not write log files.
- Treat the filesystem as ephemeral. Uploads go to object storage, state goes to a database, and anything genuinely local is a cache you can lose (Stateless Services).
- Run scheduled work as a CronJob or with leader election — never as a timer inside every replica.
- Declare a PodDisruptionBudget so voluntary disruptions (node drains, upgrades) cannot take the service below serving capacity.
What can go wrong
- CrashLoopBackOff with a healthy image, caused by missing configuration — visible only in the container log, which is deleted with the container unless it is being collected.
- A liveness probe that calls the database: a dependency slowdown becomes a fleet-wide restart, which multiplies the load on the dependency (Retry Storms).
- Readiness that never fails, so a pod that cannot serve stays in the endpoints and takes its share of traffic as errors.
- Grace period shorter than the drain, so every rollout ends in SIGKILL despite a correct shutdown implementation.
- HPA thrashing between replica counts because the metric is noisy and no stabilisation window is configured.
- An HPA scaling on CPU for an I/O-bound service, which never scales up because waiting on a database does not consume CPU (Autoscaling a Backend).
- Every new replica opening a full-size connection pool, so scaling the deployment exhausts the database's connection limit (Connection Pools).
- Endpoint removal and SIGTERM are concurrent, so requests routed a moment before deletion arrive after the process has begun shutting down. The drain delay is the mitigation.
- Several replicas starting at once can race to run migrations if migrations are in the entrypoint; use a Job or an advisory lock (Schema Migrations from the Application Side).
- A CronJob and a still-running previous invocation can overlap unless concurrency policy forbids it — two copies of the same nightly task (Scheduled Jobs).
- The HPA can scale down a pod that has just been given work, so scale-down must go through the same graceful path as a rollout.
- Run as non-root with a read-only root filesystem and dropped capabilities where the workload allows; the pod security context is application-visible configuration, not platform trivia (Container Security and Its Limits in Security Engineering).
- Prefer projected workload identity over long-lived credentials in Secrets. A Secret is base64-encoded, not encrypted, and is readable by anything with permission to read Secrets in the namespace (ConfigMap vs Secret — and the Honest Limit of a Secret in Cloud & Infrastructure).
- By default any pod can reach any other pod. Network policy is opt-in, and a backend that assumes network isolation without one is assuming something untrue (Network Segmentation in Security Engineering).
- The service account token mounted into every pod is a credential. Disable automounting where the application does not call the Kubernetes API.
- "Kubernetes makes the service resilient." It restarts things. If the application drops in-flight requests on SIGTERM, Kubernetes will do that reliably, on schedule, forever.
- "Liveness and readiness are both health checks." They cause opposite actions. A liveness probe that checks a dependency converts a dependency outage into a self-inflicted restart storm.
- "Limits are for safety, so set them tight." A tight CPU limit is a latency regression that is invisible on utilisation graphs; a tight memory limit is a kill with no error.
- "The HPA handles scaling." It handles replica count from one metric. Whether that metric reflects the load your service actually feels is entirely your problem (Autoscaling a Backend).
- "Pods are like small servers." They are terminated routinely — by drains, evictions, rollouts and autoscaling. Every property that depends on a pod surviving is a bug waiting for a node upgrade.
Operating it
- Alert on pod restart count and on
OOMKilledspecifically. A service that restarts every twenty minutes can look completely healthy in request metrics. - Track readiness-probe failures as a first-class signal; they precede almost every rollout problem.
- Watch CPU throttled seconds alongside CPU usage. Throttling with low usage means the limit is the problem, not the code (CPU Saturation: When Cores Become the Queue in Observability & Performance).
- Compare replica count against your own saturation metric over time — the honest test of whether the HPA signal reflects real load.
- Emit the pod name, node and image digest with every log line so a per-pod problem is separable from a fleet-wide one (Structured Logging).
- Replica count multiplies everything the pod holds: connections, in-process caches, outbound request rate. Scaling the deployment scales the pressure on every shared dependency by the same factor (Horizontal vs Vertical Scaling).
- At high replica counts, rollout duration and probe configuration dominate deploy time, and image pull becomes the limiting factor in how fast capacity can appear.
- Cluster-level concerns — node autoscaling, scheduling latency, priority and preemption — start intruding on application latency at scale, which is where the platform boundary needs a real owner.
- Kubernetes gives a uniform deployment API and self-healing, and charges a permanent operational tax: cluster upgrades, controller behaviour, networking, and a manifest surface that is itself a source of outages.
- Every knob it exposes is a knob you must now set correctly. Resource limits, probes and grace periods are application decisions that did not exist before.
- For one service and one team, it is usually more machinery than the problem justifies (Kubernetes Is Not Always Needed in Cloud & Infrastructure).
Where this applies
Backend advice is context-sensitive. These labels say what each claim is specific to, and where a different stack or scale would differ.
- CLOUD-SPECIFICThe Kubernetes object model is portable; the integration around it is not. Ingress controllers, load-balancer provisioning, storage classes, node autoscaling and workload identity are managed-service specific, and manifests that work on one managed Kubernetes often need changes on another.
- SIMPLIFIEDDeployments, Services, probes and resources only. StatefulSets, operators, service meshes, admission control and scheduling constraints are deliberately out of scope — this is what an application engineer must know, not what a platform engineer must know.
- SCALE-SPECIFICJustified when many services need a uniform platform. For a single service with one team the same reliability is achievable with far less machinery (Deployment Models).
Where the depth lives
This domain teaches the application-side mechanism and hands the rest off.