Stateless vs. Stateful: What's the Difference?

A stateless component handles every request in isolation: it keeps nothing between calls, so any instance can serve any request. A stateful component remembers something from one interaction to the next, which ties a request to the specific instance holding that memory. The distinction decides how a system scales, how it fails, and how it is deployed.

The phrase "stateless architecture" is misleading, though, and it is worth being blunt about why: a stateless service has not eliminated state — it has moved it somewhere else. Shopping carts, login sessions, and request counters all still exist. They have simply been relocated out of application memory and into a shared store that every instance can reach. Understanding stateless design is largely a matter of understanding where the state went and what it costs to keep it there.

What "State" Actually Means

State is any data a component retains between operations that affects how it handles the next one. That covers more than most people count at first:

  • Session state. Who the caller is, what is in their cart, which permissions they hold.
  • Conversational state. A multi-step flow — a checkout wizard, an FTP connection, a shopping session — where step 4 only makes sense given steps 1 through 3.
  • Cached data. A local copy of something expensive to recompute.
  • Coordination state. Which client holds a lock, how many requests a user has made this minute, whether a given operation already ran.
  • Application data. Orders, accounts, ledger entries — the durable record the business runs on.

A stateless service holds none of these locally. It receives everything it needs in the request itself, or fetches it from an external store, does its work, and forgets. A stateful service keeps at least one of them in its own memory or on its own disk.

Stateless vs. Stateful at a Glance

StatelessStateful
Where state livesExternally, or in the requestIn the instance's memory or disk
Request routingAny instance can serve any requestRequests must return to the same instance
ScalingHorizontal — add identical instancesHarder; needs partitioning or replication
Instance failureTraffic reroutes; nothing is lostWhatever that instance held is lost or unavailable
DeploymentRolling restarts, disposable nodesOrdered restarts, stable identity, attached storage
Latency profileNetwork hop to the store on the hot pathLocal memory access, no round trip
Main riskThe shared store becomes a bottleneck and a dependencySticky routing, data loss, uneven load
Typical examplesREST APIs, CDNs, function handlers, most microservicesDatabases, caches, message brokers, game and chat servers

Read down the two columns and the trade-off becomes clear. Stateless services buy operational freedom — disposable instances, trivial horizontal scaling, painless deploys — by paying a network round trip on every request and by making an external store a hard dependency. Stateful services keep the speed of local memory and pay for it in operational rigidity.

State Doesn't Disappear — It Moves

The classic failure this design solves is easy to describe. A user logs in; their session is stored in the heap of whichever server handled the request. The load balancer then sends their next request to a different server, which has never seen that session, and the user is silently logged out. The usual first fix is sticky sessions, pinning each user to one server — but that only defers the problem. The pinned server still restarts during deploys, still crashes, and still ends up with an uneven share of the traffic, because users cannot be rebalanced once they are bound.

The durable fix is to move the session out of the process. Once it lives in a shared in-memory store, every instance can read it, the load balancer is free to route anywhere, and a restarting node takes nothing with it. The application servers become stateless; the state is still there, just relocated.

Stateful app servers state inside the process · sticky routing required Load balancer App server 1 session: user A App server 2 session: user B App server 3 session: user C sticky User B must always return to server 2. Restart server 2 and that session is gone. Stateless app servers state externalized · route anywhere Load balancer App server 1 holds nothing App server 2 holds nothing App server 3 holds nothing any Valkey / Redis sessions · locks counters · cache Any server can serve any user. Restarting a server loses nothing — but the store is now critical. The state did not disappear. It moved out of the application and into a store every instance shares. "Stateless" describes the application tier, never the system as a whole.

Where the State Actually Goes

Running more than one instance of a service breaks every piece of local state at once, and each break has the same shape: something that was correct inside one JVM is now wrong across several. The list below is essentially a map of what has to be externalized before a service can honestly be called stateless.

  • Sessions. A user's identity and cart move from the heap to a shared session store. See web session for the underlying mechanics, and Spring Session or Tomcat session replication for the two common Java routes.
  • Mutual exclusion. The synchronized keyword and ReentrantLock are scoped to a single JVM's memory model, so they simply stop meaning anything once a second instance exists. Guarding a shared resource then requires a distributed lock whose ownership is recorded outside all of them — otherwise the classic race condition reappears, with two nodes each convinced they hold the lock.
  • Rate limiting. Limits are the case where local state fails silently rather than loudly. Each instance counts only the requests routed to it, so the effective ceiling multiplies by however many instances happen to be running — and autoscaling quietly raises it further. A rate limiter backed by a shared store keeps one budget regardless of fleet size.
  • Caching. Local caches are not wrong so much as wasteful and inconsistent: each instance pays its own warm-up cost, and an invalidation applied on one node leaves the others serving stale data. Distributed caching resolves both by keeping a single authoritative copy.
  • Deduplication. "Have I already processed this payment?" cannot be answered from local memory. Idempotency keys have to live in a store every instance can check.
  • Counters and sequences. Anything monotonic or aggregate — IDs, running totals, leaderboards — needs a single authoritative home.

This is why microservices and in-memory data stores appear together so consistently. The architecture makes application instances disposable, and something still has to hold the state they gave up.

Stateless Isn't Automatically Better

Stateless design is the right default for the application tier, but "stateless" is not a synonym for "good," and treating it as one leads to real problems. Moving state out has costs that are easy to overlook:

  • A network hop on the hot path. Local memory access is measured in nanoseconds; a round trip to a remote store is measured in fractions of a millisecond, on every request. Usually acceptable, occasionally not — a near-cache that keeps a local copy is the standard remedy for read-heavy workloads.
  • A new critical dependency. The store now sits in front of every request. If it goes down, the "resilient" stateless tier fails completely. Replication and failover are not optional once sessions live there.
  • Serialization overhead. Everything crossing the boundary has to be serialized and deserialized. Object graphs that were free to pass around in-process become a bandwidth and CPU cost, and changing a stored class between deploys can break sessions written by the previous version. See serialization.
  • Bigger requests. The other way to be stateless is to push state to the client — a signed token carrying claims rather than a session ID. That removes the lookup but inflates every request, and revoking a token before it expires means reintroducing server-side state anyway.
  • Consistency questions you didn't have before. One process reading its own memory is trivially consistent. Several processes reading a replicated store are subject to replication lag and the trade-offs described by the CAP theorem.

What Legitimately Stays Stateful

Every system bottoms out in something stateful. The goal is never to eliminate state but to concentrate it in components built to hold it, so the rest of the system can stay disposable.

  • Databases and data stores. Purpose-built for durable state, with replication, failover, and persistence as first-class features. An in-memory database such as Valkey or Redis is stateful by definition — see Redis persistence for how that state survives a restart.
  • Message brokers and event logs. Queues, topics, and streams retain messages and consumer offsets between calls.
  • Long-lived connection servers. Chat, multiplayer games, collaborative editors, and video calls hold per-connection state deliberately, because round-tripping it externally on every frame would be far too slow.
  • Stateful stream operators. Windowed aggregations, joins, and deduplication accumulate a running result that must survive restarts — a distinction covered in stream processing.

Kubernetes encodes exactly this split. Stateless workloads run as Deployment objects with interchangeable Pods, while stateful ones run as StatefulSet objects with stable network identities, ordered rollouts, and persistent volumes that follow a Pod across reschedules. Getting the classification wrong in either direction is a common source of production incidents — see connecting to a Valkey or Redis cluster on Kubernetes for the practical details.

Stateless Java Services With Valkey and Redis

In the JVM the transition from stateful to stateless is usually mechanical: a local collection or lock is replaced by its distributed equivalent. Redisson exposes Valkey and Redis through familiar java.util and java.util.concurrent interfaces, so the change is often a one-line swap rather than a redesign.

Local State That Breaks Under a Load Balancer

This counter is correct with one instance and quietly wrong with two — each JVM counts only the attempts that happened to land on it, so a five-attempt limit becomes ten across two instances.

// Stateful: the count lives in this JVM's heap.
// Two instances behind a load balancer keep two different counts.
private final Map<String, Integer> attempts = new ConcurrentHashMap<>();

public boolean tooManyAttempts(String userId) {
    return attempts.merge(userId, 1, Integer::sum) > 5;
}

The Same Logic, Externalized

Moving the map into the store makes the instance stateless. RMap implements java.util.Map, so the surrounding code is unchanged; addAndGet() performs the increment atomically in the store rather than in local memory.

// Stateless: the instance holds nothing.
// Every instance reads and writes the same value.
RMap<String, Integer> attempts = redisson.getMap("login:attempts");

public boolean tooManyAttempts(String userId) {
    return attempts.addAndGet(userId, 1) > 5;
}

Use RMapCache instead when entries need to expire on their own — a per-entry TTL is what turns "attempts ever" into "attempts in the last fifteen minutes" without a cleanup job.

Coordination Across Instances

Mutual exclusion is the case where local state fails most visibly. RLock implements java.util.concurrent.locks.Lock, but the lock is held in the store, so it is honoured by every instance rather than by one JVM. The lease parameter matters: it releases the lock automatically if the holder crashes mid-operation.

// A synchronized block guards one JVM. RLock guards the whole cluster.
RLock lock = redisson.getLock("order:" + orderId);

// Wait up to 5s to acquire; auto-release after 30s if this node dies
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
    try {
        reserveLastItemInStock(orderId);
    } finally {
        lock.unlock();
    }
}

The same pattern covers shared quotas. A rate limiter defined in the store applies one global budget across every instance, regardless of how many are running:

RRateLimiter limiter = redisson.getRateLimiter("api:global");
// 100 permits per second, shared cluster-wide
limiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.SECONDS);

if (limiter.tryAcquire(1)) {
    // handle the request
} else {
    // over the limit — reject with 429
}

Sessions Without Sticky Routing

Sessions are the one case where the migration needs no application code at all. Redisson's Tomcat Session Manager is configured in the container rather than in your code, so an existing WAR — including legacy and non-Spring applications — keeps calling HttpSession exactly as before while the data moves into Valkey or Redis. It writes changed attributes individually instead of rewriting the whole session on each request, which matters once sessions carry more than a user ID. Spring applications get the same result through Spring Session, and Redisson PRO layers a near-cache on top that answers session reads from local heap — recovering most of the round trip that externalizing the session introduced.

Together these are what make rolling deploys, autoscaling, and node failure invisible to logged-in users. For the full walkthrough see scalable session management for Java microservices and Redis-based Tomcat session management, or the Redisson and Redisson PRO feature comparison.

Stateless vs. Stateful: Frequently Asked Questions

Is HTTP Stateful or Stateless?

HTTP is stateless by design: each request carries everything the server needs to understand it, and the protocol itself retains nothing between requests. Cookies, session identifiers, and tokens are conventions layered on top of HTTP precisely because the protocol provides no memory of its own. The protocol is stateless; the applications built on it usually are not.

Is a REST API Stateful or Stateless?

Statelessness is one of the formal constraints of REST — the server stores no client context between requests, so each one must be self-contained. This is frequently misread as "REST APIs store no data." They store plenty; the resources they expose are durable and live in a database. What a REST API does not keep is per-client session context on the server between calls.

Are Microservices Stateless or Stateful?

Most microservices are deliberately built stateless so they can be scaled and replaced freely, with their state pushed into shared databases, caches, and session stores. But a microservices architecture as a whole is very much stateful — it simply concentrates the state in dedicated services rather than spreading it across every instance. Some services are stateful on purpose, including anything holding long-lived connections or windowed stream state.

What Is an Example of a Stateful Application?

Databases are the clearest example: their entire purpose is retaining data between operations. Others include message brokers holding queued messages and consumer offsets, in-memory stores such as Valkey and Redis, chat and multiplayer game servers tracking live connections, and any checkout or wizard flow whose current step depends on the steps before it.

Is Stateless Better Than Stateful?

Neither is universally better; they solve different problems. Stateless is the right default for the application tier because it makes instances disposable and horizontal scaling trivial. Stateful is correct wherever data must persist or where a network round trip per operation would be too slow. Well-designed systems use both, keeping the application tier stateless and concentrating state in components built to hold it.

Do Stateless Applications Store No Data?

No — this is the most common misconception about the term. A stateless application stores no data locally, between requests. The data still exists; it lives in an external store that every instance shares. "Stateless" describes where the state is kept, not whether it exists.

How Do Stateless Applications Handle User Sessions?

Two approaches. Either the session lives in a shared external store and the client holds only a session ID in a cookie, or the state travels with the client in a signed token that the server validates without a lookup. The first keeps requests small and allows immediate revocation but adds a round trip; the second removes the lookup at the cost of larger requests and much harder revocation.

What Are Sticky Sessions and Why Avoid Them?

Sticky sessions (session affinity) configure the load balancer to pin each user to the server that first handled them, so in-memory session data stays reachable. It works, but it makes that server a single point of failure for those users, prevents load rebalancing, and turns every deploy into a wave of logouts. Externalizing sessions removes the need for stickiness altogether.

Related Terms