DevConnect worked great on localhost, then deployment exposed the real engineering work: sleeping servers, reconnect logic, CORS, token expiry, and broken assumptions about message delivery.
This post distills the production lessons that only showed up after the app hit real infrastructure.
01What Broke First: Socket.io in Production
On free-tier infrastructure, the backend could sleep and force every client to reconnect cold. Room membership stored in memory disappeared with each restart.
const socket = io(SERVER_URL, {
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
timeout: 20000,
});
socket.on('reconnect', () => {
socket.emit('rejoin-rooms', { userId: currentUser.id });
});The server-side fix was to keep room membership in Redis so reconnects could rebuild socket state after restarts.
02The CORS Nightmare
Splitting frontend and backend across different domains turned CORS into a real security decision instead of a local annoyance.
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`CORS blocked: ${origin}`));
}
},
credentials: true,
}));03JWT Token Expiry in the Real World
Short-lived access tokens are correct, but without refresh token rotation they degrade into random user logouts and silent failures.
async function refreshTokens() {
const res = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include',
});
if (!res.ok) {
logout();
return null;
}
const { accessToken } = await res.json();
return accessToken;
}04Chat Events Arriving Out of Order
Concurrent emits do not always arrive in the order you mentally expect. DevConnect fixed that by tagging each message with a sequence number and sorting on the client before rendering.
io.to(roomId).emit('chat:message', {
...message,
seq: ++sequence,
});
messageBuffer.push(msg);
messageBuffer.sort((a, b) => a.seq - b.seq);05Environment Variables: The Gap Between Dev and Prod
Configuration drift caused failures that were invisible until deploy time, so the app moved to startup validation instead of hoping every environment variable existed.
const REQUIRED_ENV = [
'MONGODB_URI',
'JWT_ACCESS_SECRET',
'JWT_REFRESH_SECRET',
'REDIS_URL',
'CLIENT_URL',
];
function validateEnv() {
const missing = REQUIRED_ENV.filter((key) => !process.env[key]);
if (missing.length) process.exit(1);
}06What Production Actually Taught Me
- Localhost hides latency, crashes, and state loss.
- Error handling and structured logs are not optional.
- Real-time apps need state outside process memory.
- Free-tier infrastructure constraints should shape the design up front.