Stateless vs Stateful Services in AWS: Differences, Examples, and When to Use Each
A stateless service treats every request as a fresh start; a stateful service remembers what happened before. This single difference controls how easily a service scales, how it survives instance failures, and how much operational effort it takes to run reliably in production.
Statelessness is one of the most important design properties in cloud architecture. A stateless compute tier scales horizontally without coordination: add more instances, point the load balancer at them, and you are done. A stateful compute tier creates routing constraints, uneven load distribution, and fragile failover paths that are easy to build accidentally and painful to fix later.
This page covers application and service architecture: whether a service stores client-specific memory between requests. It is not about the networking sense of “stateful” and “stateless,” which describes whether a firewall tracks connection state. That is a separate concept covered on the Security Groups Explained page.
The difference in plain English
A stateless service does not remember anything between requests. When a request arrives, the server reads what it needs from the incoming data or from a shared external store, processes the request, and returns a response. The server then forgets the interaction entirely. The next request from the same user is treated identically to a request from a new visitor.
A stateful service carries memory between requests. Server-side sessions are the most common example: the user logs in, the server creates a session object and stores it in memory, and every subsequent request depends on retrieving that in-memory object to work correctly.
A stateless service is like a vending machine. It does not remember you, does not care who you are, and any identical machine gives you the same result. A stateful service is like a personal bank teller who keeps notes on your open requests and account history. Helpful, but you cannot seamlessly switch to a different teller mid-conversation without losing context.
What stateless and stateful mean in AWS
Stateless compute means the EC2 instance, Lambda function, or container does not hold any client-specific data in local memory or on local disk. All state lives in an external store: a database, a cache, or object storage. Any instance can handle any request because they all read from the same shared backend.
Stateful compute means the compute resource holds data specific to one or more clients. A Node.js server storing express-session objects in memory is a classic example. PHP’s default session handling (writing session files to /tmp) is another. Both work fine on a single server; both break the moment you add a second server behind a load balancer.
Making your compute stateless does not eliminate state from your system. It moves state to a dedicated stateful service such as DynamoDB, RDS, ElastiCache, or S3, where it is managed, replicated, and durable by design. Every working web application has stateful components somewhere. The goal is to confine statefulness to services that were built to handle it, not to eliminate it entirely.
Why the distinction matters
Scaling. A stateless service scales horizontally without constraints. The Auto Scaling Group adds instances and the load balancer routes to them immediately. A stateful service can only scale if the new instance has access to existing state, which requires replication, session migration, or sticky routing.
Load balancing. The Application Load Balancer distributes requests evenly across healthy targets. If sessions are stored on individual instances, even distribution breaks the session, so the ALB must use sticky sessions to pin each user to one instance. That defeats even distribution and creates hotspots.
Failover. When an instance fails, the ASG terminates it and starts a replacement. A stateless service survives this transparently: the next request hits a healthy instance and works. A stateful service loses every session stored on the failed instance.
Deployments. Blue/green and rolling deployments are straightforward for stateless services because instances are interchangeable. Stateful services require session draining, state migration, or a shared session store to handle deployments without user-visible disruption.
Operational complexity. Stateless compute instances are fungible. You replace them freely without worrying about what data they hold. Stateful services require backups, replication, failover configuration, and careful capacity planning for the state they own.
How it works in practice on AWS
The standard AWS web architecture is: EC2 instances in an Auto Scaling Group, behind an Application Load Balancer, spread across multiple Availability Zones. When you first build this with two instances, the statefulness problem becomes immediate.
Here is what happens when sessions are stored locally and traffic hits two instances:
- The user logs in. The request routes to Instance A. Instance A creates a session and stores it in memory.
- The user clicks to another page. The ALB routes to Instance B (round-robin). Instance B has no session for this user.
- Instance B redirects the user to the login page. The user is confused. The team thinks the login system is randomly broken.
Enabling session affinity on the ALB target group pins each user to a specific instance via a cookie. The random-logout problem disappears, and the team moves on. This is the wrong path. Sticky sessions hide the statefulness behind routing — the session still lives on one instance. When the ASG scales in and terminates that instance, every user pinned to it loses their session simultaneously. Uneven traffic distribution also means some instances become overloaded while others sit idle.
The correct fix is to externalize session state: move sessions out of instance memory and into a shared store that every instance can reach.
Stateless vs stateful: side-by-side comparison
| Dimension | Stateless | Stateful |
|---|---|---|
| Where state lives | External store (DB, cache, object storage) | Server memory or local disk |
| Horizontal scaling | Add instances freely; any handles any request | Requires routing constraints or state migration |
| Failover behavior | Seamless; next request goes to a healthy instance | Sessions on the failed instance are lost |
| Load balancing | Even distribution works correctly | Requires sticky sessions or session migration |
| Deployment complexity | Low; instances are interchangeable | Higher; requires draining or session-aware rollout |
| Operational burden | Low; no per-instance state to manage | Higher; state must be backed up and replicated |
| Latency trade-offs | Small overhead for external state reads | Zero overhead for in-memory reads on that one instance |
| Common AWS examples | EC2 + ALB + DynamoDB sessions, Lambda, ECS tasks | RDS, ElastiCache, EFS, Amazon MQ |
| Typical use cases | APIs, web apps, microservices, event pipelines | Databases, caches, queues, shared filesystems |
Real AWS example: moving sessions out of EC2
Here is a concrete before-and-after for a Node.js Express application moving from local in-memory sessions to DynamoDB.
Before: sessions stored on EC2 (breaks under multiple instances)#
const express = require('express');
const session = require('express-session');
const app = express();
app.use(session({
secret: 'my-secret-key',
resave: false,
saveUninitialized: false
// Default MemoryStore: sessions live in this process's memory only.
// Works on a single instance. Fails silently when a second instance starts.
}));The default MemoryStore in express-session exists only in the memory of the process that created it. A second EC2 instance has its own memory with no knowledge of the first instance’s sessions.
After: sessions stored in DynamoDB (works across any number of instances)#
const express = require('express');
const session = require('express-session');
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
// connect-dynamodb is a common session store adapter.
// Verify the current API for your express-session version before use.
const connectDynamoDB = require('connect-dynamodb');
const DynamoDBStore = connectDynamoDB(session);
const app = express();
const client = new DynamoDBClient({ region: 'us-east-1' });
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: new DynamoDBStore({
client: client,
table: 'app-sessions'
})
}));Every instance now reads and writes session data from the same DynamoDB table. Any instance can handle any user’s request. You can add or remove instances without affecting any active sessions.
Creating the DynamoDB sessions table#
The table needs a partition key (sessionId as a string) and a TTL attribute to auto-expire old records.
aws dynamodb create-table \
--table-name app-sessions \
--attribute-definitions AttributeName=sessionId,AttributeType=S \
--key-schema AttributeName=sessionId,KeyType=HASH \
--billing-mode PAY_PER_REQUEST
aws dynamodb update-time-to-live \
--table-name app-sessions \
--time-to-live-specification Enabled=true,AttributeName=expiresDynamoDB TTL deletes records automatically once the timestamp in the expires attribute has passed. Without TTL, the sessions table grows indefinitely: every expired session still occupies storage and counts toward your billed read/write units. Set the expiry window to match your session timeout. 24 hours is a common default. TTL deletions incur no additional charge.
For a deeper look at DynamoDB’s data model and query patterns, see the DynamoDB Overview.
When to use stateless
REST and GraphQL APIs. APIs are naturally stateless when authentication is handled via tokens (JWT or API keys) sent in the request header. The token carries the identity, so no server-side session is needed.
Horizontally scaled web applications. Any web tier behind an ALB and an ASG should be stateless. This is the primary pattern on AWS for scalable web applications, and statelessness is what makes horizontal scaling transparent.
AWS Lambda. Lambda functions cannot reliably maintain state between invocations. Execution environments are created and destroyed by AWS on demand. All input must come from the event payload or be fetched from an external store at invocation time.
Microservices. Each service in a microservices architecture should be independently deployable and scalable. Statelessness at the service level is what makes independent scaling practical without coordination overhead.
Event-driven pipelines. In event-driven systems, each event carries its own context. The worker that processes it does not need memory of previous events, making event-driven compute inherently stateless and straightforward to scale.
Containers in ECS. Containers are ephemeral by nature: they start, do work, and stop. Stateless containers fit this model perfectly. Stateful containers require additional volume management and complicate scheduling.
When stateful is the right choice
Stateful services are not an anti-pattern. They are a category of purpose-built infrastructure that every system depends on. The question is not “how do I avoid stateful services?” but “which layer should own the state?”
Relational databases. RDS is inherently stateful and designed to be. It stores structured data with ACID guarantees, replication, and automated backups. Your compute tier stays stateless by reading from and writing to RDS. The database owns the durable state. See RDS Backups and High Availability for how AWS manages that statefulness for you.
Caches. ElastiCache (Redis or Memcached) stores computed results, session data, and frequently accessed objects. Its value comes precisely from its statefulness: it holds warm data so your compute does not re-fetch it on every request.
Message queues. SQS durably stores messages until a consumer processes them. This statefulness is what makes queues reliable: messages are not lost if a consumer dies mid-processing.
Shared filesystems. EFS provides a POSIX filesystem that multiple EC2 instances can mount simultaneously. For workloads that genuinely require shared file access such as CMS platforms, HPC jobs, or legacy applications, EFS is the appropriate tool. For choosing between object storage and a shared filesystem, see S3 vs EFS.
Multi-step workflows. Some workloads require durable, coordinated state across many steps such as order processing pipelines, approval workflows, and batch jobs with checkpoints. Rather than building this on a stateful EC2 instance, AWS Step Functions manages the workflow state for you while keeping the individual task workers stateless.
For choosing between key-value and relational storage for your stateful data layer, see RDS vs DynamoDB.
Common mistakes
- Using sticky sessions as a permanent fix. Sticky sessions mask the problem without solving it. Load distribution becomes uneven, and when an instance is terminated, every user pinned to it loses their session at once. Externalize state instead.
- Storing file uploads on EC2 local disk. If a user uploads a file and it lands on one instance’s disk, the file is gone when that instance is replaced by the ASG. Store uploads directly to Amazon S3 using pre-signed URLs, or move files to S3 immediately after receipt on the server.
- No TTL on DynamoDB session records. Without TTL, the sessions table grows indefinitely. Every expired session still occupies storage. Always enable DynamoDB TTL with an expiry window that matches your session timeout (24 hours is a common default).
- Misunderstanding Lambda warm environment reuse. Lambda reuses warm execution environments between invocations, so module-level variables initialised outside the handler persist between calls on the same environment. This is useful for caching database clients. It is dangerous if you use it to carry user-specific state: a different user’s invocation may read another user’s data left in a shared module-level variable.
- Assuming stateless compute means no state anywhere. Some teams over-correct and try to avoid all persistent state, leading to convoluted workarounds. The correct model is stateless compute reading from and writing to purpose-built stateful services. DynamoDB, RDS, ElastiCache, and S3 exist precisely to hold state reliably.
Stateless vs stateful vs sticky sessions
These three terms are sometimes conflated, but they describe different things:
Stateless service: no per-client memory between requests. Any instance handles any request. Scales freely with no routing constraints.
Stateful service: per-client memory stored on the server in memory or on disk. Follow-up requests from the same client must reach the same instance, or the state is gone.
Sticky sessions: a load balancer feature that routes each client’s requests to the same backend instance using a cookie. Sticky sessions do not make a stateful service stateless. They are a routing patch that hides statefulness from the load balancer. The service is still stateful: sessions still live on one instance and are still lost if that instance is terminated.
The correct architecture flows in one direction: externalize state → use stateless compute → let the load balancer distribute freely. Sticky sessions skip the “externalize state” step and treat the symptom rather than the cause.
For designing highly available systems on AWS, stateless compute across multiple AZs behind a load balancer is the foundation. Sticky sessions undermine that foundation by reintroducing instance affinity.
Frequently asked questions
What makes a service stateless?
A stateless service does not store any per-client information between requests. Every request contains everything the server needs — authentication token, user ID, parameters — and the server processes it without relying on memory from previous interactions. If you can terminate any running instance and the next request succeeds on a different instance without the user noticing, the service is stateless.
Are Lambda functions really stateless?
Lambda is stateless by design — AWS creates and destroys execution environments on demand, and you cannot control which environment handles a given invocation. However, Lambda does reuse warm execution environments, which means module-level code runs once and persists across invocations on the same environment. This is useful for caching database clients, but do not use it to carry user-specific state between invocations — that state is not guaranteed to be there, and it may be visible to a different user's invocation on the same environment.
Can I use sticky sessions instead of externalizing state?
You can, but sticky sessions are a workaround, not a fix. They route each user to the same backend instance via a cookie, which hides the statefulness from the load balancer but does not remove it. When an instance is terminated by an Auto Scaling Group, every user pinned to it loses their session simultaneously. Load distribution also becomes uneven. Externalizing state to DynamoDB or ElastiCache is the correct solution.
DynamoDB vs Redis for sessions: which is better?
Both work well. DynamoDB is fully managed, requires no cluster configuration, and has built-in TTL — it is the simpler starting point. Redis (ElastiCache) reads at sub-millisecond latency and supports richer data structures, but requires more operational effort. If you are already running ElastiCache for caching, consolidating sessions there makes sense. If not, DynamoDB is the lower-overhead choice for most applications.
When is a stateful design the better choice?
When data must persist beyond a single request and belongs in a purpose-built store. Databases (RDS, DynamoDB), caches (ElastiCache), queues (SQS), and shared filesystems (EFS) are all correctly stateful — they are designed to hold and manage state reliably. The goal is not to eliminate statefulness from your system; it is to confine state to services that handle it well rather than letting it accumulate on compute instances that were not designed for it.