How to Implement Stateless Node.js Applications for Horizontal Scaling
A stateless Node.js application stores no mutable data inside the process, instead keeping all state in external databases, caches, or object stores, enabling any instance to handle any request and allowing effortless horizontal scaling.
Horizontal scaling requires running multiple interchangeable instances behind a load balancer. The goldbergyoni/nodebestpractices repository emphasizes that true statelessness means ensuring no single Node.js process retains data that another instance would need to access, as documented in sections/production/bestateless.md. By externalizing all state, you eliminate server affinity and enable zero-downtime deployments.
What Makes a Node.js Application Stateless?
A stateless design means that no request-processing instance keeps any mutable data that other instances need to see. All state lives outside the Node.js process – in databases, caches, object stores, or other external services. When an application is truly stateless, it can be replicated arbitrarily, enabling effortless horizontal scaling, zero-downtime deployments, and rapid recovery from failures.
According to the repository's README section 5.12, keeping data in-process adds routing complexity and makes restarts costly. The "Be stateless" section explicitly recommends storing any data outside the process to maintain instance interchangeability.
Core Principles for Stateless Architecture
| Principle | Implementation | Scaling Benefit |
|---|---|---|
| No local persistence | Avoid writing files, uploading data, or storing sessions on the server's filesystem. | New instances can serve traffic immediately without requiring a "local copy" of data. |
| Externalize state | Use SQL/NoSQL databases, distributed caches (Redis, Memcached), object storage (AWS S3, GCS), or message brokers. | All instances read/write from the same source, keeping them interchangeable. |
| Stateless authentication | Use JWTs containing all claim information verified by shared secrets, or store revocation blacklists in external stores (e.g., Redis) rather than in-process memory. | Any instance can verify authentication without session affinity. |
| Idempotent request handling | Design APIs so repeating requests does not corrupt data (use PUT with identifiers, generate deterministic IDs). |
Load balancers can safely retry requests on other nodes when servers fail. |
| Graceful shutdown | Drain connections and finish in-flight requests before exiting. | Orchestrators (Kubernetes, ECS) can add or remove instances without dropping traffic. |
| External configuration | Store configuration in environment variables or config services (AWS Parameter Store). | New containers start with identical configuration without manual edits. |
Why Stateless Architecture Enables Horizontal Scaling
Statelessness directly facilitates horizontal scaling through four key mechanisms:
- Add instances on demand – Since each instance can serve any request, load balancers distribute traffic evenly without requiring "sticky sessions".
- Zero-downtime deployments – Deploy new versions and terminate old pods without data loss because state is not tied to specific hosts.
- Resilience – If a node crashes, other nodes continue serving traffic because state lives in external services (e.g., Redis).
- Cost efficiency – Autoscaling provisions the exact number of containers needed for current load, allowing use of low-cost spot instances.
Implementation Examples
The following patterns from goldbergyoni/nodebestpractices demonstrate how to remove local state from common Node.js operations.
Externalize File Storage with AWS S3
Instead of writing uploads to local disk, stream directly to object storage so any instance can process subsequent requests for that file.
const AWS = require('aws-sdk');
const multer = require('multer');
const multerS3 = require('multer-s3');
// Configure S3 client (external store)
const s3 = new AWS.S3({
accessKeyId: process.env.AWS_ACCESS_KEY,
secretAccessKey: process.env.AWS_SECRET,
region: process.env.AWS_REGION,
});
// Multer storage that streams directly to S3
const upload = multer({
storage: multerS3({
s3,
bucket: process.env.UPLOAD_BUCKET,
acl: 'private',
key: (req, file, cb) => cb(null, `${Date.now()}_${file.originalname}`),
}),
});
app.post('/photos/upload', upload.array('photos', 12), (req, res) => {
res.json({ uploaded: req.files.map(f => f.location) });
});
All uploaded data lives in S3, so any server can handle uploads without local storage dependencies.
Share Sessions via Redis
Replace in-memory session stores with Redis to allow any instance to authenticate users without server affinity.
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('redis');
const redisClient = redis.createClient({
url: process.env.REDIS_URL,
});
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true, maxAge: 3600000 },
})
);
Session data lives in Redis, enabling any instance to read it and eliminating the need for sticky sessions.
Stateless JWT Authentication with External Revocation
Use JWTs for stateless verification while maintaining security through externalized revocation lists, as detailed in sections/security/expirejwt.md.
const jwt = require('express-jwt');
const jwtBlacklist = require('express-jwt-blacklist');
// Use Redis for the blacklist (external, shared across nodes)
jwtBlacklist.configure({
store: new jwtBlacklist.stores.RedisStore({ url: process.env.REDIS_URL }),
});
app.use(
jwt({
secret: process.env.JWT_PUBLIC_KEY,
algorithms: ['RS256'],
isRevoked: jwtBlacklist.isRevoked,
})
);
Tokens are verified without server-side state, but revocation checks are centralized in Redis, preserving statelessness while enabling security controls.
Implement Graceful Shutdown Handlers
Ensure instances can be safely terminated during scaling events by handling the SIGTERM signal properly.
const http = require('http');
const server = http.createServer(app);
process.on('SIGTERM', () => {
console.log('SIGTERM received – closing server...');
server.close(() => {
console.log('Server closed. Exiting.');
process.exit(0);
});
// Force exit after 30 seconds
setTimeout(() => process.exit(1), 30000);
});
server.listen(process.env.PORT || 3000);
When a container stops, it ceases accepting new connections and finishes in-flight requests, allowing orchestrators to safely scale down.
Key Files in the Node.js Best Practices Repository
The following files in goldbergyoni/nodebestpractices contain the authoritative guidance referenced in this article:
sections/production/bestateless.md– Core "Be stateless" guidance and anti-patterns.sections/production/productioncode.md– Lists "Be stateless" as a strategic production practice.sections/security/expirejwt.md– Demonstrates keeping JWTs stateless while using external blacklist stores.README.md(section 5.12) – TL;DR summary and rationale for stateless design.
Summary
- Stateless applications store no mutable data in the Node.js process, placing all state in external services.
- Externalize files to object storage (S3), sessions to Redis, and authentication revocation to shared stores.
- Graceful shutdown handlers allow orchestrators to safely terminate instances during scaling events.
- Idempotent APIs enable safe request retries across multiple instances.
- Following the patterns in
goldbergyoni/nodebestpracticesensures your application can scale horizontally without server affinity.
Frequently Asked Questions
What is the difference between stateful and stateless Node.js applications?
A stateful application stores user data, sessions, or files locally within the Node.js process or filesystem, requiring subsequent requests from the same user to hit the same server. A stateless application keeps all mutable data in external stores (databases, caches, object storage), making every instance interchangeable and allowing load balancers to route any request to any server.
How do I handle user sessions in a stateless Node.js application?
Store session data in an external distributed cache like Redis or Memcached using libraries such as connect-redis. Configure express-session to use the external store rather than default in-memory storage. This allows any instance in your cluster to retrieve session data and authenticate users without requiring "sticky sessions" from your load balancer.
Can I use JWT authentication and still maintain stateless architecture?
Yes, JWTs are inherently stateless because they contain all necessary claims and are verified using shared secrets or public keys. However, if you need token revocation (logout functionality), you must externalize the revocation list to a shared store like Redis rather than maintaining an in-process blacklist. This approach preserves statelessness while allowing security enforcement across all instances.
Why does local file storage prevent horizontal scaling?
When applications write uploads, logs, or temporary files to the local filesystem, those files exist only on the specific server that handled the request. If a subsequent request for that file is routed to a different instance, the resource will be missing. This creates server affinity, forcing you to implement complex sticky-session routing or file replication, which complicates auto-scaling and prevents true horizontal scalability.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →