How I Handle Authentication for 50K Users Without Losing My Mind
How I Handle Authentication for 50K Users Without Losing My Mind
Md. Rony Ahmed
· 6 min read
How I Handle Authentication for 50K Users Without Losing My Mind
Most auth tutorials stop at "it works." Mine stops at "it survives a real attack."
I used to think authentication was simple. You hash a password, sign a JWT, and call it a day.
Then I built a system that handles 50,000 active users. And I learned the hard way that "it works" and "it works at scale" are two completely different things.
This is the architecture I wish I'd found two years ago. No theory. Just the hybrid approach that survived a credential stuffing attack last month.
The Problem: JWT vs Sessions — Most Devs Pick Wrong
The internet loves JWTs. Stateless! Scalable! No database lookups!
What nobody tells you: JWTs are a liability at scale.
Approach → Works For → Dies At:
- Pure JWT → Single-page apps, short sessions → Dies at: Token revocation, password changes, logout
- Pure Sessions → Traditional web apps, monoliths → Dies at: Distributed systems, mobile apps
- Hybrid (what we use) → Everything → Dies at: Nothing — that's the point
Here's what happens when a user changes their password with pure JWTs:
1. Attacker has the old JWT (valid for 24 hours)
2. User changes password
3. Attacker keeps using the old JWT
4. You can't revoke it without maintaining a denylist
And now you're back to database lookups. Congratulations, you built sessions with extra steps.
What We Built: Short JWT + Rotated Refresh Tokens
Our hybrid approach:
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
│ Client │────▶│ API Gateway │────▶│ Auth Service│
│ │◀────│ │◀────│ │
└─────────────┘ └──────────────────┘ └──────────────┘
│ │
│ Access Token (15 min) │
│◀──────────────────────────────────────────┤
│ │
│ Refresh Token (7 days) │
│◀──────────────────────────────────────────┤
│ │
│ Token Rotation on Use │
│─────────────────────────────────────────────▶│
The Rules
1. Access tokens expire in 15 minutes — short enough that stolen tokens are useless fast
2. Refresh tokens rotate on every use — old refresh token invalidated immediately
3. Refresh tokens are single-use — replay attacks die automatically
4. Token families track lineage — if a rotated token is used twice, we nuke the whole family
5. Sessions stored in Redis with TTL — we can revoke everything in <50ms
The Implementation (Node.js + Redis)
Token Generation
// auth/token.service.js
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const redis = require('../redis-client');
class TokenService {
// Access token: short-lived, no sensitive data
generateAccessToken(userId, roles) {
return jwt.sign(
{ sub: userId, roles },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '15m', issuer: 'codehustle' }
);
}
// Refresh token: opaque, stored in Redis
async generateRefreshToken(userId, tokenFamily) {
const token = crypto.randomBytes(32).toString('hex');
const key = `refresh:${token}`;
await redis.setex(key, 7 * 24 * 3600, JSON.stringify({
userId,
family: tokenFamily || crypto.randomUUID(),
createdAt: Date.now(),
used: false
}));
return token;
}
// Rotation: invalidate old, create new
async rotateRefreshToken(oldToken) {
const key = `refresh:${oldToken}`;
const data = await redis.get(key);
if (!data) {
// Token reused — possible theft
const parsed = JSON.parse(data);
await this.revokeTokenFamily(parsed.family);
throw new Error('Token reuse detected');
}
const parsed = JSON.parse(data);
if (parsed.used) {
// Double-use detected — nuke the family
await this.revokeTokenFamily(parsed.family);
throw new Error('Token replay attack detected');
}
// Mark old token as used
await redis.del(key);
// Generate new refresh token in same family
const newToken = await this.generateRefreshToken(
parsed.userId,
parsed.family
);
return newToken;
}
async revokeTokenFamily(familyId) {
// Pattern match all tokens in family
const keys = await redis.keys(`refresh:*`);
for (const key of keys) {
const data = await redis.get(key);
if (data) {
const parsed = JSON.parse(data);
if (parsed.family === familyId) {
await redis.del(key);
}
}
}
}
}
Rate Limiting on Auth Endpoints
The attack vector most tutorials ignore:
// middleware/auth-rate-limit.js
const redis = require('../redis-client');
const authRateLimit = async (req, res, next) => {
const ip = req.ip;
const email = req.body.email;
// Per-IP limit: 5 attempts per minute
const ipKey = `ratelimit:ip:${ip}`;
const ipAttempts = await redis.incr(ipKey);
if (ipAttempts === 1) await redis.expire(ipKey, 60);
if (ipAttempts > 5) {
return res.status(429).json({
error: 'Too many attempts. Try again in 1 minute.'
});
}
// Per-email limit: 3 attempts per 15 minutes
// This prevents distributed brute force
const emailKey = `ratelimit:email:${email}`;
const emailAttempts = await redis.incr(emailKey);
if (emailAttempts === 1) await redis.expire(emailKey, 900);
if (emailAttempts > 3) {
// Log for security monitoring
await logSecurityEvent('brute_force_attempt', { email, ip });
return res.status(429).json({
error: 'Account locked. Check your email.'
});
}
next();
};
The Breach That Almost Happened
Last month at 3:47 AM, our monitoring fired:
ALERT: 847 login attempts for user rony@codehustle.tech
Source: 23 different IP addresses
Pattern: Credential stuffing attack
Here's what happened:
1. 847 attempts against one account in 4 minutes
2. 23 rotating IPs — clearly a botnet
3. Per-email rate limiting kicked in at attempt #4
4. Account locked — attacker couldn't proceed
5. Token rotation meant even if they'd succeeded, existing sessions were safe
Without our hybrid approach:
- Pure JWTs? Attacker gets a 24-hour token, changes password, still has access
- Pure sessions? Database lookups at 847 req/min = performance death
- Hybrid? 847 requests, 3 got through rate limit, 0 succeeded.
The Numbers
Metric — Before (Pure JWT) — After (Hybrid):
- Token revocation time — 24 hours (impossible) — <50ms
- DB lookups per auth check — 0 — 0 (Redis)
- Session hijack window — 24 hours — 15 minutes
- Password change safety — ❌ Old tokens valid — ✅ Instant revocation
- Brute force resistance — ❌ None — ✅ Per-email limiting
5-Minute Security Audit Checklist
Run this before your next deploy:
1. ✅ Token rotation works — log in, use refresh token twice, verify second attempt fails
2. ✅ Rate limiting on auth endpoints — hit /login 10 times fast, verify 429 response
3. ✅ Password change invalidates sessions — change password, verify old tokens die
4. ✅ Redis TTL matches token expiry — verify no orphaned tokens after 7 days
5. ✅ Alert on token reuse — set up monitoring for "Token replay attack detected"
The Real Lesson
Authentication isn't about picking JWT or sessions. It's about controlling session lifecycle.
Most tutorials show you how to sign a token. They don't show you what happens when:
- A user gets phished
- A password database leaks
- A nation-state targets your users
Build for the attack, not the tutorial.
Want the full production config? I share detailed implementation guides on the CodeHustle blog.
Built with Node.js, Redis, and the scars of 2 AM security incidents.