Socket.io: Safari dropping web socket connection due to inactivity when page not in focus

Created on 25 Apr 2017  ·  26Comments  ·  Source: socketio/socket.io

You want to:

  • [x] report a bug
  • [ ] request a feature

Current behaviour

Not sure if this is a known issue (I tried searching but found nothing). Safari for Mac appears to be silently dropping websocket connections due to inactivity/idle if the page/tab is not in focus.

Steps to reproduce (if the current behaviour is a bug)

Make Safari tab/page not in focus; log websocket events.

Expected behaviour

Websockets should be kept alive via the heartbeat functionality. Not seeing this behavior in other browsers so unlikely to be my code.

Setup

  • OS: Mac OSX 10.12.4 (16E195)
  • browser: Safari 10.1 (12603.1.30.0.34)
  • socket.io version: 1.7.3

Other information (e.g. stacktraces, related issues, suggestions how to fix)

Is this possibly some sort of power-saving feature that is overriding/ignoring the heartbeats?

bug

Most helpful comment

I think that's because the current version of socket.io relies on the setTimeout on the client side, which may not be as reliable as expected.

We'll include this in the v3, as it is a breaking change.

Related: https://github.com/primus/primus/issues/348

All 26 comments

Running server with DEBUG=* shows the following:
socket.io:client client close with reason ping timeout +0ms
socket.io:socket closing socket - reason ping timeout +0ms

which I presume means that Safari closed the connection, not the socket server or client instance. However the strangest thing is that I've noticed Safari sometimes reconnects around 30 seconds to 1 minute later but other times it does not and stays disconnected until the page is brought to focus. It's incredibly frustrating to try and debug with such inconsistent behavior.

It seems that sometimes it sporadically reconnects much later (like 10 minutes). Again, completely inconsistent under identical testing environments.

@twistedpixel the reconnection delay is exponential (that is, something like: wait 500ms, try reconnect, wait 1000ms, try reconnect...) (source), so that may explain the behaviour.

How about forcing to reconnect when the window gets the focus again?

window.addEventListener("focus", () => socket.connect());

It may be related to https://github.com/primus/primus/issues/348.

Thanks for the info but the main issue is that I need the web socket permanently connected because it's used to send alerts to the user while they're away. Therefore window focus to reconnect isn't ideal.

I think it's actually something more problematic specific to my machine/install anyway. I noticed the behaviour originally on my iMac so I decided to just wipe my MacBook with a fresh version of Safari and I'm not seeing the behavior there at all. I left a tab minimised for a whole day and it didn't disconnect once. I therefore tried going back to the iMac and removing all internet plugins and disabling all extensions but still I saw this behavior.

Apple doesn't seem to provide any way to reinstall Safari completely other than deleting its preferences and certain other files. Or wiping the machine. Part of me wants to just start fresh but the developer in me would hate not knowing what the cause is.

Actually, to your point about the exponential re-connects: surely the first re-connect would, as you say, be around 500ms after disconnect... so why would the server ignore it? There must be something preventing it from firing the re-connect.

It's a bit weird because if I stick a socket.connect() in the disconnect event, it connects again just fine. It has to do it every few minutes but still it does so without fail. So I'm completely puzzled as to why a re-connect isn't happening! I'll do a bit more digging and see if I can figure out why.

This is typical browser behavior nowadays, even on desktops, unfortunately.

I think I know what's happening. Safari is indeed the problem.

I think all browsers cap the setTimeout and setInterval values at 1000 when the tab is not in focus. Safari - stupidly - caps it at 1000 and does something like exponentially adding a delay that results in each iteration taking doubly as long as the last. This is why the connection dies; socket.io's internal timeouts are being delayed/dropped, explaining why reconnects aren't happening when they should.

So basically, Apple has decided to go against the grain, as usual, resulting in a poor user experience. They're really good at this these days.

I haven't discovered why it's affecting the iMac and not the MacBook (I'd have expected the reverse) but I'll keep testing and see if I can pinpoint the exact reason.

@twistedpixel it's not only Safari. See http://blog.strml.net/2017/01/chrome-56-now-aggressively-throttles.html

In Primus we worked around the issue by reversing the direction of the heartbeat messages (https://github.com/primus/primus/pull/534).

@lpinca The entire time I was trying to figure out this issue I was wondering that very thing. Thanks for the info! I have been looking at Primus but I didn't want to have to refactor my entire code base so soon. Seems it could be worth the effort though.

@twistedpixel my point is that the same can probably be done in Engine.IO so there is no need to migrate to Primus.

FWIW, Safari Tech Preview does not seem to be affected by the additional throttling. Perhaps Apple reversed their decision. It's still throttling to 1000ms but doesn't seem to be adding anything further.

I am experiencing the same issue on iOS 12 Safari. If I re-open my safari, the websocket connection is gone. Is there a clean workaround to keep the socket alive?

AFAIK iOS Safari suspends certain processes when Safari is backgrounded (to prevent battery drain) and websocket connections are almost certainly one such process. It’s unlikely you’ll find a workaround on mobile devices.

OK. But I still can reconnect if I add an event listener like onwindowfocus or something?

Has anyone implemented a workaround? we are interested in looking at options and wonder if others are already experimenting

Rather than using focus events, you'll need to use the Page Visibility API to detect when a mobile app window has been backgrounded.

I bumped into the issue with Azure SignalR and thanks to @techpeace suggestion currently using the Page Visibility API to close the connection on page hide and reconnect it again when the page is visible.But bumped into problems with quick switching of tabs, which can send multiple events. Currently looking into debouncing the events..Also general advice found on the web discourages any kind of handling based on user agent detection..so my solution is to use the page visibility API irrespective of the user agent.

Solutions

I tested for several hours with all 3 of these browsers, changing the pingTimeout & pingInterval values. What I found to be solutions:

  1. Setting pingTimeout >= 30000ms

    • or -

  2. Setting pingInterval <= 10000ms

I believe the best solution to be changing pingTimeout = 30000. The default pingInterval is 25000ms and increasing the frequency of the server pinging the clients to every 10s can be too chatty for _at scale_ projects.

I think that's because the current version of socket.io relies on the setTimeout on the client side, which may not be as reliable as expected.

We'll include this in the v3, as it is a breaking change.

Related: https://github.com/primus/primus/issues/348

@darrachequesne i ma also facing that when on mobile if my phone screen goes in standby and i open the browser again on mobile, socket io disconnects the chat. plz fix this. it will be huge relieve.

any update on this bug in socket io?

in my app when user tries to upload a file from their mobile browser, and when upload dialog box opens, socket io disconnects them if they take 15 seconds or more to select a file.

if they switch to another page or tab, after 15 sec , socket io again disconnects them, is there anyway to fix this and keep socket io alive/connected even if user is not on page/document focused?

@darrachequesne

I have fixed this issue with Visibility API.

Main issue with Safari for me - it has no time to close socket on visible.hidden === true, so you need to close websocket after device is unlocked, and initiate it again.

@JustFly1984 Do you have some sample code for that. I've got the visibility detection working properly, but I can't get the socket to reconnect.

So now this is happening with MacOS Safari too, FYI.

@calendee @anilanar we are not using sockets.io, just pure websockets, and also we are using React.js, so the code is pretty complex. the main idea that we have two <ContextProvider /> for each api, visibility is on top, websockets on the bottom, and websockets using context from visibility.

Thanks for getting back to me JustFly1984. Actually, in the end, I didn't need the visibility API. I simply need to add timeouts. Once I did this, I no longer had the connection issues on iOS Safari.

// Establish a Socket.io connection
// Initialize our Feathers client application through Socket.io
// with hooks and authentication.
client.configure(feathers.socketio(socket), {
  timeout: 2000,
});
// Use localStorage to store our login token
client.configure(feathers.authentication(), {
  timeout: 2000,
});
Was this page helpful?
0 / 5 - 0 ratings