Statelessness
Objectives
By the end of this lesson, you should be able to:
- Demonstrate a session-based login breaking across two separate instances
- Demonstrate a JWT-based login working correctly across the same two instances
- Explain what makes an application genuinely stateless
💡 Why this matters: Course 3 built both session-based authentication (Module 2) and JWT-based authentication (Module 3), and noted a comparison between them. This lesson is where that comparison becomes concrete, and consequential, one of them breaks the moment there’s more than one instance, the other doesn’t.
⚠️ A note on verification: every snippet and every output in this lesson was actually run, using two genuinely separate Express app instances, standing in for two separate servers behind a load balancer.
Simulating Two Separate Instances
const instanceA = createSessionApp('A');
const instanceB = createSessionApp('B');
Two entirely separate express() applications, each with its own express-session middleware, each with its own, independent in-memory session store, exactly what running the same application on two separate servers would look like, no memory shared between them at all.
A Session-Based Login, Across Two Instances
const agent = request.agent(instanceA);
const loginRes = await agent.post('/login');
console.log('Login (routed to instance A):', loginRes.body);
const profileFromA = await agent.get('/profile');
console.log('Next request, routed to instance A again:', profileFromA.body);
const cookie = loginRes.headers['set-cookie'];
const profileFromB = await request(instanceB).get('/profile').set('Cookie', cookie);
console.log('Same cookie, routed to instance B:', profileFromB.body);
Login (routed to instance A): { instance: 'A', message: 'logged in', sessionID: 'l8iX-...' }
Next request, routed to instance A again: { instance: 'A', userId: 42 }
Same cookie, routed to instance B: { instance: 'B', error: 'no session found on this instance' }
The exact same session cookie, sent to instance A, works correctly, sent to instance B, fails entirely, req.session.userId simply doesn’t exist there, because that session was only ever stored in instance A’s own memory. In a real deployment, a load balancer (Lesson 3) has no reason to route every request from the same client to the same instance, unless it’s specifically configured to (a technique called “sticky sessions,” with its own real costs, covered in Lesson 4), this failure would happen intermittently, in production, exactly when it’s hardest to debug.
A JWT-Based Login, Across the Same Two Instances
const loginRes = await request(instanceA).post('/login');
const token = loginRes.body.token;
const profileFromA = await request(instanceA).get('/profile').set('Authorization', `Bearer ${token}`);
console.log('Same request, routed to instance A:', profileFromA.body);
const profileFromB = await request(instanceB).get('/profile').set('Authorization', `Bearer ${token}`);
console.log('Same token, routed to instance B instead:', profileFromB.body);
Login (routed to instance A): { instance: 'A', token: 'eyJhbGciOiJIUzI1NiIs...' }
Same request, routed to instance A: { instance: 'A', userId: 42 }
Same token, routed to instance B instead: { instance: 'B', userId: 42 }
The exact same token works correctly on both instances, jwt.verify() needs only the shared secret (Course 3, Module 3, Course 4, Module 1), never anything stored in one specific instance’s memory, this is what “stateless” actually means, every instance can independently verify the exact same request, correctly, with nothing shared between them except a secret they were all configured with identically.
What Makes This Work
The JWT itself carries everything needed to verify it, jwt.verify() is a pure function of the token and the secret, no lookup, no shared memory required, an in-memory session store, by contrast, requires the specific instance holding that session’s data to be the one handling the request, or a shared store (Redis, Module 7 of this course, is a common choice) every instance can reach instead.
Try It
- Build both demos above, and confirm the exact failure and success patterns shown.
- Explain, in your own words, why
jwt.verify(token, secret)doesn’t need to know anything about which instance issued the token. - Identify a fix for the session-based version that doesn’t involve switching to JWTs, hint, Module 7’s Redis, used as a shared session store instead of the in-memory default.
- Explain, in one or two sentences, why “stateless” doesn’t mean an application has no state at all, just that it doesn’t require a specific instance’s own memory to serve a request correctly.
Recap
- A session stored only in one instance’s memory fails on any other instance, confirmed here with the exact same cookie succeeding on instance A and failing on instance B.
- A JWT, verified with a shared secret and no per-instance memory, works correctly on any instance, confirmed here with the same token succeeding on both.
- Statelessness, or a shared store for whatever state can’t be avoided, is what makes horizontal scaling actually safe, this is precisely why Course 3 favored JWTs as this track’s primary authentication method.
Next lesson: building a working load balancer, and observing requests actually distributed across multiple instances.