25

I'm building an App deployed to Heroku which uses Websockets.

The websockets connection is working properly when I use only 1 dyno, but when I scale to >1, I get the following errors

POST http://****.herokuapp.com/socket.io/?EIO=2&transport=polling&t=1412600135378-1&sid=zQzJJ8oPo5p3yiwIAAAC 400 (Bad Request) socket.io-1.0.4.js:2

WebSocket connection to 'ws://****.herokuapp.com/socket.io/?EIO=2&transport=websocket&sid=zQzJJ8oPo5p3yiwIAAAC' failed: WebSocket is closed before the connection is established. socket.io-1.0.4.js:2

I am using the Redis adaptor to enable multiple web processes

var io = socket.listen(server);
var redisAdapter = require('socket.io-redis');
var redis = require('redis');

var pub = redis.createClient(18049, '[URI]', {auth_pass:"[PASS]"});
var sub = redis.createClient(18049, '[URI]', {detect_buffers: true, auth_pass:"[PASS]"} );

io.adapter( redisAdapter({pubClient: pub, subClient: sub}) );

This is working on localhost (which I am using foreman to run, as Heroku does, and I am launching 2 web processes, same as on Heroku).

Before I implemented the redis adaptor I got a web-sockets handshake error, so the adaptor has had some effect. Also it is working occasionally now, I assume when the sockets match the same web dyno.

I have also tried to enable sticky sessions, but then it never works.

var sticky = require('sticky-session');
sticky(1, server).listen(port, function (err) {
  if (err) {
    console.error(err);
    return process.exit(1);
  }
  console.log('Worker listening on %s', port);
});
Smittey
  • 2,475
  • 10
  • 28
  • 35
Jack Wild
  • 2,072
  • 6
  • 27
  • 39
  • I did some more exploration of this problem lately, I discovered that if you specify the port when you visit your .herokuapp.com address (ie. yourapp.herokuapp.com:80) the sockets connection works. Although obviously not a practical solution for production! It helps in staging. – Jack Wild Nov 10 '14 at 10:47
  • Remy Sharp looks at this in a blog post https://remysharp.com/2014/11/10/muddling-my-way-through-real-time#server-side – Joe Nov 10 '14 at 15:14
  • Thanks, I'll have a read through that. – Jack Wild Nov 10 '14 at 15:24

5 Answers5

20

I'm the Node.js Platform Owner at Heroku.

WebSockets works on Heroku out-of-the-box across multiple dynos; socket.io (and other realtime libs) use fallbacks to stateless processes like xhr polling that break without session affinity.

To scale up socket.io apps, first follow all the instructions from socket.io:

Then, enable session affinity on your app (this is a free feature):

hunterloftis
  • 13,386
  • 5
  • 48
  • 50
  • 2
    hey, hunter. do you have any info on the number of concurrent socket.io connections i could expect to handle per dyno? (ballpark is fine). thanks. – Jonah Nov 25 '15 at 03:52
  • 2
    This works for me! I did what @hunterloftis suggested, I'm using [socket.io-redis](https://github.com/socketio/socket.io-redis) and session affinity in heroku and all works great :) – carloscarcamo Apr 07 '16 at 14:44
  • @Jonah did you ever end up figuring out the load ballpark? Interested as I have the same problem currently –  Jan 26 '17 at 02:25
  • @DaemonOfTheWest i don't think i did, iirc. we ended up using pusher – Jonah Jan 26 '17 at 02:28
  • Hi @hunterloftis I know your post is old but I'm hoping you could answer a quick question. I'm able to get a WebSocket server running using socket.io but can I use Ws library instead? Or does Heroku only support socket io? – Irelia May 12 '19 at 17:06
  • @Nina you can use ws as well. Here's an old test repo with a demo: https://github.com/hunterloftis/websocket-ping – hunterloftis May 13 '19 at 03:36
  • @hunterloftis Coincidently I just found that repo right before you posted your comment and deploys and runs just fine. Thank you! Another question, why is the WS address different than a socket io address? A socket io address can look like this testapp.herokuapp.com/socket.io/?EIO=4&transport=websocket while a WS address can just look like this ws://testapp.herokuapp.com – Irelia May 13 '19 at 03:43
  • i tried all that but it didn't work using this commit https://github.com/ltfschoen/flappytips/commit/8aaaadc3dbd178969ca2dfd3f4607c035f57dd6d. i followed the instructions that you mentioned https://socket.io/docs/v4/using-multiple-nodes/#using-nodejs-cluster where i allowed credentials on client and server, and applied the example shown to create a Node.js Cluster using @socket.io/sticky. i was using Heroku's Basic paid plan, and enabled Heroku Session Affinity as you mentioned, and ran `heroku ps:scale web=1:Basic; heroku features:enable http-session-affinity; heroku ps:restart; heroku open` – Luke Schoen Feb 17 '23 at 00:39
6

I spent a while trying to make socket.io work in multi-server architecture, first on Heroku and then on Openshift as many suggest.

The only way to make it work on both PAAS is disabling xhr-polling and setting transports: ['websocket'] on both client and server.

On Openshift, you must explicitly set the port of the server to 8000 (for ws – 8443 for wss on socket.io client initialization, using the *.rhcloud.com server, as explained in this post: http://tamas.io/deploying-a-node-jssocket-io-app-to-openshift/.

Polling strategy doesn't work on Heroku because it does not support sticky sessions (https://github.com/Automattic/engine.io/issues/261), and on Openshift it fails because of this issue: https://github.com/Automattic/engine.io/issues/279, that will hopefully be fixed soon.

So, the only solution I found so far, is disabling polling and use websocket transport only.

In order to do that, with socket.io > 1.0 server-side:

var app = express();
var server = require('http').createServer(app);

var socketio = require('socket.io')(server, {
  path: '/socket.io-client'
});
socketio.set('transports', ['websocket']);

client-side:

var ioSocket = io('<your-openshift-app>.rhcloud.com:8000' || '<your-heroku-app>.herokuapp.com', {
    path: '/socket.io-client'
    transports: ['websocket']
})

Hope this will help.

qubird
  • 529
  • 1
  • 6
  • 4
  • I've been searching for a solution for the same problem for almost a week, even got in touch with Heroku support team, but nothing's have been solved. I tried what you mentioned here and it finally Worked!. Thank you, @qubird . – Serhan Oztekin Oct 08 '15 at 15:35
  • Im using the Redis adapter for SocketIO & was getting the handshake error. Specifying the websocket transport was the solution. Thank you!! – thedeadlybutter Dec 10 '16 at 17:57
1

It could be you need to be running RedisStore:

var session = require('express-session');
var RedisStore = require('connect-redis')(session);

app.use(session({
    store: new RedisStore(options),
    secret: 'keyboard cat'
}));

per earlier q here: Multiple dynos on Heroku + socket.io broadcasts

Community
  • 1
  • 1
aug2uag
  • 3,379
  • 3
  • 32
  • 53
  • Yup you do need to use the Redis adapter and Redis Store, I already am doing this, but it's still not helping. – Jack Wild Nov 10 '14 at 10:44
1

I know this isn't a normal answer, but I've tried to get WebSockets working on Heroku for more than a week. After many long conversations with customer support I finally tried out OpenShift. Heroku WebSockets are in beta, but OpenShift WebSockets are stable. I got my code working on OpenShift in under an hour.

http://www.openshift.com

I am not affiliated with OpenShift in any way. I'm just a satisfied (non-paying) customer.

Luc
  • 3,581
  • 3
  • 22
  • 24
1

I was having huge problems with this. There were a number of issues failing simultaneously making it a huge nightmare. Make sure you do the following to scale socket.io on heroku:

  1. if you're using clusters make sure you implement socketio-sticky-session or something similar
  2. client's connect url should not be https://example.com/socket.io/?EIO=3&transport=polling but rather https://example.com/ notably I'm using https because heroku supports it

  3. enable cors in socket.io

  4. specify only websocket connections

For you and others it could be any one of these.

if you're having trouble setting up sticky-session clusters here's my working code

var http = require('http');
var cluster = require('cluster');
var numCPUs = require('os').cpus().length;
var sticky = require('socketio-sticky-session');
var redis = require('socket.io-redis');
var io;

if(cluster.isMaster){
  console.log('Inside Master');
  // create the worker processes
  for (var i = 0; i < numCPUs ; i++){
    cluster.fork();
  }
}
else {
  // The worker code to be run is written inside
  // the sticky().
}

sticky(function(){
  // This code runs inside the workers.
  // The sticky-session balances connection between workers based on their ip.
  // So all the requests from the same client would go to the same worker.
  // If multiple browser windows are opened in the same client, all would be
  // redirected to the same worker.
  io = require('socket.io')({transports:'websocket', 'origins' : '*:*'});
  var server = http.createServer(function(req,res){
    res.end('socket.io');
  })


  io.listen(server);
  // The Redis server can also be used to store the socket state
  //io.adapter(redis({host:'localhost', port:6379}));

  console.log('Worker: '+cluster.worker.id);
    // when multiple workers are spawned, the client
    // cannot connect to the cloudlet.

    StartConnect(); //this function connects my mongodb, then calls a function with io.on('connection', ..... socket.on('message'...... in relation to the io variable above

    return server;
}).listen(process.env.PORT || 4567, function(){
  console.log('Socket.io server is up ');
});

more information: personally it would work flawlessly from a session not using websockets (I'm using socket.io for a unity game. It worked flawlessly from the editor only!). When connecting through the browser whether chrome or firefox it would show these handshaking errors, along with error 503 and 400.

Community
  • 1
  • 1
hydrix
  • 332
  • 2
  • 9