$ emrebener

06. Processes

author: emre bener read time: 7 min about: twelve-factor app, shared-nothing architecture, scalability
published: updated: mentions: redis, memcached, kubernetes, aws lambda, load balancing, statefulset

1. The rule: run the app as stateless, share-nothing processes

The factor is one sentence: execute the app as one or more stateless, share-nothing processes. Stateless means a process holds no persistent state of its own between requests. Share-nothing means no process depends on state sitting in another process; they don’t coordinate through shared memory, and none of them is special. Any state that has to outlive a single request or job goes somewhere else.

That somewhere else is a stateful backing service, almost always a database, which makes this factor the direct partner of Backing services. Factor 4 turns every datastore into an attached resource behind a config handle; Factor 6 is what makes that pay off. Because the app keeps nothing durable of its own, all of its durable state lives in those attached resources, and the process itself becomes small and disposable. As the series introduction put it, a twelve-factor process is stateless and holds no local state it can’t afford to lose.

That last qualifier matters, because the factor doesn’t say a process can never touch its own memory or disk. It can. A process’s memory and filesystem are fair game as a single-transaction scratch cache: download a file, work on it in a temp directory, write the result to a datastore, all inside one request or job. What the app must never do is assume anything cached that way survives to a future request. The scratch space is real, but it’s scratch. The moment the transaction ends, treat everything local as gone.

2. Why local process state can’t be trusted

State that lives only in a process is state you’re about to lose. Two things guarantee it, and both are baked into how a twelve-factor app runs.

The first is concurrency. A twelve-factor app scales by running many identical processes, so there’s rarely just one. Put a load balancer in front of a fleet of them and the next request from a given user is likely to land on a different process than the last one did. Whatever the first process stashed in memory, the second one never saw. State written to one process is invisible to every other process in the fleet.

The second is restarts, and this one gets even a lone process. Processes get restarted constantly and for ordinary reasons: you ship a code deploy, you change a config value or the platform relocates the process onto another machine to pack its nodes better. Every one of those wipes the process’s local memory and starts fresh, and a relocation wipes its local filesystem too. This is the same disposability that Disposability treats as a feature: a process you can kill and restart at any moment is only safe to kill if it wasn’t holding anything you needed. Statelessness is the precondition that makes killing it safe.

So local state fails twice over. Across processes, the next request won’t see it. Within one process, the next restart erases it. Anything you keep in a process’s memory or on its disk, expect to lose, and usually sooner than you’d think.

3. The canonical violation: sticky sessions

The classic way to break this factor is sticky sessions. A user logs in, the app stores their session data in the memory of whichever process handled the login, and from then on a load balancer routes that user back to the same process on every request so the session is there waiting. It works in a demo. It’s a twelve-factor violation, and you shouldn’t rely on it.

Here’s what it costs you. The session now lives in exactly one process’s memory, so the moment that process goes away, every user pinned to it is logged out at once. And processes go away all the time: one crashes, or a routine deploy rolls the fleet, and a chunk of your users are bounced to the login screen for no reason they can see. It breaks scaling in both directions, too. You can’t safely scale the fleet down, because removing a process drops the sessions of everyone pinned to it. And a load balancer routing on affinity can’t rebalance an uneven load, because it isn’t free to move a busy user to an idle process; that’s where their state is. You’ve traded away exactly the elasticity the process model was supposed to give you.

The fix is to move the session out of the process and into a shared datastore with time-expiration, typically Redis or Memcached. The session key goes in the store and the value expires on its own after a while so stale sessions clean themselves up. Now the session belongs to no single process. Any process can look it up, so any process can serve any request, and the load balancer is free to send a user wherever it likes. The affinity requirement disappears, because there’s nothing local left to be sticky to.

Sticky sessions — violationUserLoad balancer(pins by affinity)Process Asession in RAMProcessBProcess CA dies → session gone,user logged outShared store — the fixUserLoad balancer(routes to any process)Process AProcess BProcess CShared session store(Redis / Memcached)any process serves any requestaffinitySticky sessions — violationUserLoad balancer(pins by affinity)Process Asession in RAMProcessBProcess CA dies → session gone,user logged outShared store — the fixUserLoad balancer(routes to any process)Process AProcess BProcess CShared session store(Redis / Memcached)any process serves any requestaffinity

4. Statelessness is what lets an app scale out

Everything cloud-native does to scale assumes stateless processes. Because any process can serve any request, identical replicas are interchangeable, and interchangeable is what makes them cheap to move around. You can add replicas under load and take them away when it drops. You can load-balance across them with no affinity, sending each request to whichever process is free. You can autoscale on demand, and you can lose a whole node without losing anything but its in-flight requests, because the state was never on that node to begin with. None of this works if a request is pinned to state in one process’s memory. Scaling out via the process model is its own factor, Concurrency, and statelessness is the property that lets it work at all.

This is exactly what the modern platforms bank on. Kubernetes scales a Deployment by changing a replica count, spinning identical stateless pods up and down; an autoscaling group does the same with virtual machines behind a load balancer. Serverless takes it furthest of all. A function-as-a-service invocation, an AWS Lambda call, may get a completely fresh execution context with nothing retained from the last one, so you can’t assume anything you set aside is still there when the next invocation runs. That’s the factor taken to its logical extreme: not just stateless between requests, but potentially a clean slate on every single one.

5. “Stateless” doesn’t mean “no state anywhere”

The word “stateless” overpromises. It means the app tier isn’t the source of truth for any state, not that state exists nowhere. Of course it exists; it’s in the database, the cache, the queue. What the factor forbids is the app process being the place any of it lives.

That distinction is what separates a legitimate local cache from a violation. A process is free to keep an in-memory cache as a performance optimization, as long as correctness never depends on that cache surviving. If a value vanishing just means the next request recomputes or refetches it, the cache is doing its job, and losing it costs a little latency and nothing else. It’s a cache, not a source of truth. The violation is depending on local state, not keeping it. Sticky sessions are the textbook case: the local copy is the only copy, so losing it loses the data.

And some workloads genuinely are stateful. A database holds state by definition; so does a stream processor accumulating a windowed aggregate, or a service holding long-lived WebSocket connections. The answer for those isn’t to pretend they’re stateless. It’s to make the state explicit and external, and to run them on something built to own it: a shared store, or a Kubernetes StatefulSet with stable identities and persistent volumes, rather than smuggling durable state into an app process that was supposed to be disposable. Name the stateful thing and give it a home built for state; keep everything else stateless.

That’s the discipline, and elastic scale is the prize. Push state out of the app tier and the app processes stay interchangeable and disposable, exactly the processes a platform can replicate, autoscale, relocate, and kill without a second thought. That statelessness is what makes all of that safe. Keep your processes empty of anything they can’t afford to lose, and you can run as many or as few of them as the moment needs.