Real-time apps with Socket.IO

Socket.IO gives you rooms, events, auto-reconnect, and fallbacks on top of WebSockets — the higher-level option for chat and live features. A verified server-plus-client round-trip, and an honest comparison with plain ws.

When you want live features — chat, notifications, a shared game board — you have two Node.js options: the low-level ws library, or Socket.IO, which builds named events, rooms, automatic reconnection, and fallbacks on top. This guide gets a Socket.IO round-trip running on your one Falix port and is honest about when the extra layer earns its keep.

At a glance
You need A server running the Node.js application
Background Your first web app and, ideally, Real-time apps with WebSockets
Plan Any plan
Time Twenty-five minutes

Socket.IO vs plain WebSockets

Socket.IO uses WebSockets underneath, then adds conveniences you'd otherwise build yourself. It is not interoperable with a raw WebSocket client — both ends must speak Socket.IO.

Socket.IO Plain ws
Named events Built in (socket.on('chat', ...)) You invent a message format
Rooms / broadcasting Built in (io.to('room').emit(...)) You track clients yourself
Auto-reconnect Built in, with backoff You write it (the ws guide shows how)
Fallback when WS is blocked Falls back to HTTP long-polling None — WebSocket or nothing
Client Needs the Socket.IO client library Native browser WebSocket
Overhead A bit more per message Minimal

Pick Socket.IO when you want rooms, reconnection, and resilience without writing them — most chat and collaboration apps. Pick plain ws when you want the lightest possible connection, full control of the wire format, or a standard WebSocket that any client can speak. Both serve HTTP and real-time traffic from the same single port, which is exactly what a Falix app server gives you.

The server

Install socket.io from the Packages page (search socket.io, install, restart). Socket.IO attaches to an ordinary HTTP server — the same one that binds your public port:

const express = require('express');
const http = require('node:http');
const { Server } = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = new Server(server); // rides on the HTTP server — one port

app.get('/', (req, res) => res.send('Socket.IO server is up.'));

io.on('connection', (socket) => {
  console.log('client connected:', socket.id);

  // A named event. The client emits 'chat message'; we rebroadcast it.
  socket.on('chat message', (msg) => {
    io.emit('chat message', msg); // to everyone, including the sender
  });

  socket.on('disconnect', () => console.log('client left:', socket.id));
});

const PORT = process.env.SERVER_PORT || 8080;
server.listen(PORT, '0.0.0.0', () => console.log(`Listening on port ${PORT}`));

The shape mirrors the WebSockets guide: new Server(server) hands Socket.IO the HTTP server that's already listening, so server.listen (reading SERVER_PORT, binding 0.0.0.0) serves both your web page and the real-time channel. The difference is what you get for free — named events ('chat message' instead of parsing raw strings) and io.emit to broadcast without tracking clients by hand.

The client

Socket.IO needs its own client library (a raw browser WebSocket won't connect). In a browser, load it and connect to your public address:

<script src="/socket.io/socket.io.js"></script>
<script>
  const socket = io(); // connects back to the server that served this page
  socket.on('connect', () => socket.emit('chat message', 'hello!'));
  socket.on('chat message', (msg) => console.log('received:', msg));
</script>

Socket.IO serves that client script from /socket.io/socket.io.js automatically. Calling io() with no argument connects to the same origin — no address to hard-code when the page and server are the same app.

Verify it works

The quickest proof is a Node client that connects, sends, and receives. Install socket.io-client, then:

const { io } = require('socket.io-client');
const socket = io('http://127.0.0.1:' + (process.env.SERVER_PORT || 8080));

socket.on('connect', () => {
  console.log('connected as', socket.id);
  socket.emit('chat message', 'hello from the client');
});
socket.on('chat message', (msg) => {
  console.log('received broadcast ->', msg);
  process.exit(0);
});

Run it against the server and you'll see the full loop — the client connects, emits chat message, the server's io.emit broadcasts it, and the client logs received broadcast -> hello from the client. That round-trip is your real-time channel working.

Rooms — the reason to reach for Socket.IO

Rooms are why most people choose Socket.IO. A socket can join named rooms, and you can broadcast to just one:

io.on('connection', (socket) => {
  socket.on('join', (room) => socket.join(room));
  socket.on('message', ({ room, text }) => {
    io.to(room).emit('message', text); // only clients in that room
  });
});

That's per-channel chat, per-game lobbies, or per-document collaboration with almost no bookkeeping — the exact thing you'd hand-roll over plain ws.

Reconnection and reality

Socket.IO's client reconnects automatically with backoff, so a phone changing networks or a brief hiccup heals itself — no code from you. Two honest caveats for Falix:

⚠️ Heads up: Behind the Reverse Proxy for HTTPS, connect with the secure endpoint and let Socket.IO negotiate — it will use wss:// and fall back to secure long-polling if needed (Domains and HTTPS). And every open connection costs memory: broadcasting to thousands multiplies the work, and the free plan's 2.5 GB shared RAM fills faster than you'd expect (Out of memory).

Scaling Socket.IO across multiple processes needs a Redis adapter so broadcasts reach clients on every process — relevant only once you outgrow one process; the normal single-process case here needs nothing extra. Everything beyond the Falix layer is standard Socket.IO, documented in full at socket.io.

Troubleshooting

  • Client never connects — you're using a raw browser WebSocket instead of the Socket.IO client, or the versions differ. Both ends must be Socket.IO, and major versions must match.
  • Connects then drops immediately — the HTTP server bound localhost instead of 0.0.0.0, so nothing outside the container reaches it. Bind 0.0.0.0 and read SERVER_PORT.
  • Works locally, not in production — a hard-coded port. Use process.env.SERVER_PORT.
  • Secure page refuses to connect — an HTTPS page can't use an insecure endpoint. Serve through the Reverse Proxy so Socket.IO uses wss://.

Next steps

Was this guide helpful?