Socket.IO doesn't work, but Native Websocket works

I am developing a Next.js application with a custom server to implement WebSockets.

Initially, I used the Socket.IO library, which worked fine in a local environment. However, when I uploaded the application to Render, connection issues arose: WebSocket took a long time to establish a connection, and when it did, it resorted to long polling instead of WebSocket.

I suspected that Render had limitations with WebSockets, but when I checked the available examples, I found a real-time chat written in GoLang that worked fine. This led me to conclude that the problem might be with the SocketIO library. I then tested it with native WebSockets, and the application worked perfectly in production.

However, SocketIO offers several additional features that could be useful, as they are extensively tested and ready to use. I would like to better understand why SocketIO does not work well in this environment, while native WebSockets work without problems.

server.ts with Native Websockets:

import { createServer } from "node:http";
import next from "next";
import { WebSocketServer } from "ws";

const dev = process.env.NODE_ENV !== "production";
const hostname = "localhost";
const port = 10000;
const app = next({ dev, hostname, port });
const handler = app.getRequestHandler();

app.prepare().then(() => {
  const httpServer = createServer(handler);

  new WebSocketServer({ server: httpServer });

  httpServer.once("error", onError).listen(port, () => console.log(`> Ready on http://${hostname}:${port}`));
});

function onError(err: Error) {
  console.error(err);
  process.exit(1);
}

server.ts with Socket.IO:

import { createServer } from "node:http";
import next from "next";
import { Server } from "socket.io";

const dev = process.env.NODE_ENV !== "production";
const hostname = "localhost";
const port = 10000;
const app = next({ dev, hostname, port });
const handler = app.getRequestHandler();

app.prepare().then(() => {
  const httpServer = createServer(handler);
  new Server(httpServer, {
    transports: ["websocket"],
    httpCompression: false,
    pingInterval: 25000,
    pingTimeout: 5000,
    cors: {
      origin: process.env.NEXT_PUBLIC_URL,
      methods: ["GET", "POST"],
      credentials: true,
    },
  });

  httpServer.once("error", onError).listen(port, () => console.log(`> Ready on http://${hostname}:${port}`));
});

function onError(err: Error) {
  console.error(err);
  process.exit(1);
}

Adding some logs, I can see that the client connection reaches the server, but sending and receiving messages does not work.

Hi Cristóvão,

This is a known issue due to our lack of sticky sessions.

I’d encourage you to upvote this feature request at https://feedback.render.com/features/p/sticky-session. It helps to include as much context as possible about your use case, the problem you’re looking to solve, and how you’re getting around it today to help us develop the best possible solution.

We rely heavily on customer feedback as a part of our planning and product roadmap process, so capturing interest on the feature request page is very helpful.

Regards,

Matt