|Docs

Deploy a WebSocket Application with Socket.IO

deploymentfrontendwebsocketssocketio

Socket.IO is the most widely used library for real-time bidirectional communication in JavaScript applications. It handles WebSocket connections with automatic fallback to HTTP long-polling, built-in reconnection, and room-based message routing.

This guide covers how to deploy a Socket.IO application on Railway, handle reconnections, and scale across multiple instances.

What you will deploy

  • A Socket.IO server running on Express.
  • A frontend client that connects to the server.
  • Both services deployed to the same Railway project.

Server setup

Create a new Node.js project and install dependencies:

npm init -y
npm install express socket.io

Create server.js:

import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';

const app = express();
const httpServer = createServer(app);

const io = new Server(httpServer, {
  cors: {
    origin: process.env.FRONTEND_URL || '*',
    methods: ['GET', 'POST'],
  },
  // Start with polling, then upgrade to WebSocket
  transports: ['polling', 'websocket'],
});

// Health check endpoint
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok', connections: io.engine.clientsCount });
});

io.on('connection', (socket) => {
  console.log(`Client connected: ${socket.id}`);

  socket.on('message', (data) => {
    // Broadcast to all other clients
    socket.broadcast.emit('message', data);
  });

  socket.on('disconnect', (reason) => {
    console.log(`Client disconnected: ${socket.id}, reason: ${reason}`);
  });
});

const port = process.env.PORT || 3000;
httpServer.listen(port, '0.0.0.0', () => {
  console.log(`Socket.IO server running on port ${port}`);
});

Key configuration:

  • CORS: set origin to your frontend's domain. Use a reference variable like FRONTEND_URL=${{Frontend.RAILWAY_PUBLIC_DOMAIN}} to keep it in sync.
  • Transports: ['polling', 'websocket'] starts with HTTP long-polling and upgrades to WebSocket. This is Socket.IO's default and works reliably through Railway's proxy.
  • Health check: the /health endpoint lets Railway monitor the service. Configure it in Health Checks.

Client setup

Install the Socket.IO client in your frontend:

npm install socket.io-client

Connect to your Railway-hosted server:

import { io } from 'socket.io-client';

const socket = io('https://your-socketio-server.railway.app', {
  transports: ['polling', 'websocket'],
  reconnection: true,
  reconnectionDelay: 1000,
  reconnectionDelayMax: 5000,
  reconnectionAttempts: Infinity,
});

socket.on('connect', () => {
  console.log('Connected:', socket.id);
});

socket.on('message', (data) => {
  console.log('Received:', data);
});

socket.on('disconnect', (reason) => {
  console.log('Disconnected:', reason);
  // Socket.IO will automatically reconnect
});

Replace https://your-socketio-server.railway.app with your service's public domain.

Deploy to Railway

  1. Create a new project on Railway.
  2. Deploy the Socket.IO server from your GitHub repository or using the CLI.
  3. Generate a domain for the server.
  4. Set the FRONTEND_URL variable to your frontend's domain for CORS.
  5. Configure a health check pointing to /health.

Ensure the server binds to 0.0.0.0 and reads the port from the PORT environment variable.

Handle reconnections

WebSocket connections on Railway are exempt from inactivity timeouts and can stay open indefinitely. Connections can still drop for other reasons, such as deploys or network interruptions.

Socket.IO handles reconnection automatically. When the connection drops, the client reconnects and receives a new socket.id. The reconnection settings in the client configuration above ensure this happens with exponential backoff.

To preserve state across reconnections:

  1. Use rooms for group membership. When a client reconnects, re-join the rooms it was in:
// Server
io.on('connection', (socket) => {
  socket.on('join-room', (roomId) => {
    socket.join(roomId);
  });
});

// Client
socket.on('connect', () => {
  // Re-join rooms after reconnection
  socket.emit('join-room', currentRoomId);
});
  1. Use a last-seen timestamp. On reconnection, the client sends the timestamp of the last message it received. The server sends any messages the client missed.

Scale with a Redis adapter

By default, Socket.IO routes messages through the server's in-process memory. If you scale to multiple instances, clients connected to different instances cannot communicate.

The Redis adapter solves this by routing messages through Redis pub/sub.

  1. Add a Redis database to your Railway project.
  2. Install the adapter:
npm install @socket.io/redis-adapter redis
  1. Configure the server:
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();

await Promise.all([pubClient.connect(), subClient.connect()]);

io.adapter(createAdapter(pubClient, subClient));

Set REDIS_URL using a reference variable pointing to your Redis service's connection string.

With the Redis adapter in place, messages broadcast on one instance reach clients connected to all instances.

Common pitfalls

WebSocket upgrade fails, connection stays on polling. This can happen if your client or a proxy between the client and Railway does not support WebSocket upgrades. Socket.IO falls back to polling automatically, which works but is less efficient. To debug, check the transport property:

socket.on('connect', () => {
  console.log('Transport:', socket.io.engine.transport.name);
  socket.io.engine.on('upgrade', () => {
    console.log('Upgraded to:', socket.io.engine.transport.name);
  });
});

CORS errors on connection. The Socket.IO server must explicitly allow your frontend's origin. Set the origin in the cors option. Using '*' works for development but should be restricted in production.

Sticky sessions not configured for multiple instances. Without the Redis adapter, clients that reconnect may hit a different instance that does not have their session. The Redis adapter eliminates this issue. If you cannot use Redis, enable sticky sessions by routing based on the Socket.IO session ID, though this is more complex to configure.

Next steps