Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Safari dropping web socket connection due to inactivity when page not in focus #2924

Closed
1 of 2 tasks
twistedpixel opened this issue Apr 25, 2017 · 28 comments
Closed
1 of 2 tasks
Labels
bug Something isn't working
Milestone

Comments

@twistedpixel
Copy link

twistedpixel commented Apr 25, 2017

You want to:

  • 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?

@twistedpixel
Copy link
Author

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.

@twistedpixel
Copy link
Author

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

@darrachequesne
Copy link
Member

@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 primus/primus#348.

@twistedpixel
Copy link
Author

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.

@twistedpixel
Copy link
Author

twistedpixel commented Apr 28, 2017

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.

@llaakso
Copy link

llaakso commented May 1, 2017

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

@twistedpixel
Copy link
Author

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.

@lpinca
Copy link

lpinca commented May 2, 2017

@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 (primus/primus#534).

@twistedpixel
Copy link
Author

@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.

@lpinca
Copy link

lpinca commented May 3, 2017

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

@twistedpixel
Copy link
Author

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.

@username99987
Copy link

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?

@twistedpixel
Copy link
Author

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.

@username99987
Copy link

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

@roynasser
Copy link

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

@techpeace
Copy link

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

@niwsa
Copy link

niwsa commented Feb 10, 2019

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.

@crobinson42
Copy link

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 -
  1. 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.

@darrachequesne
Copy link
Member

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: primus/primus#348

@darrachequesne darrachequesne added the bug Something isn't working label Mar 20, 2019
@FaizanZahid
Copy link

@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.

@FaizanZahid
Copy link

FaizanZahid commented Oct 31, 2019

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

@JustFly1984
Copy link

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.

@calendee
Copy link

@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.

@anilanar
Copy link

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

@JustFly1984
Copy link

JustFly1984 commented Jul 28, 2020

@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.

@calendee
Copy link

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,
});

@darrachequesne
Copy link
Member

For future readers: this should be fixed in latest versions due to two changes:

  • the heartbeat mechanism has been reversed in Socket.IO v3

The server now sends PING packets, so the client will not be declared dead when it does not send a PING in time due to timer throttling.

See also: https://socket.io/blog/engine-io-4-release/#heartbeat-mechanism-reversal

  • this fix, included in socket.io-client@^4.1.0

@volkanger
Copy link

Year 2023. This is still happening with websockets, I'll try @JustFly1984's method. or I'll use socket.io, I guess.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests