WebTech1223411 logo WebTech1223411Web tech, read closely
Games

Why Browser Games Freeze in Background Tabs

A few years ago I looked after the backend for a browser card game with a few thousand players online at peak. Our disconnect rate looked too high, and the client team swore the network code was fine.

A row of browser tabs with one paused game tab dimmed in the background

A few years ago I looked after the backend for a browser card game with a few thousand players online at peak. Our disconnect rate looked too high, and the client team swore the network code was fine. When I finally matched server logs against the client's visibility events, about 17 percent of the disconnects came from players who had simply switched to another tab for a few minutes. Nothing was wrong with their connection. The browser had decided our game was not important while it was hidden, and our heartbeat stopped beating.

Browsers throttle background tabs on purpose, to save battery and CPU, and they have become steadily stricter about it. For a normal web page that is invisible. For a real time game it means timers drift, the game loop stops, and the server may conclude the player has left. You cannot switch this behaviour off, but you can design around it, and most of the work is on the side that people forget: the server.

What the browser actually does to a hidden tab

The rules differ by browser and change over time, so treat these as the behaviour I have measured in recent Chrome and Firefox, not a specification. Safari on iOS is stricter still and may freeze a hidden page entirely.

MechanismVisible tabHidden tabHidden for over 5 minutes (Chrome)
requestAnimationFrameEvery display framePaused completelyPaused completely
setTimeout and setIntervalAs scheduledAt most about once per secondChained timers may wake only once per minute
WebSocket messagesDeliveredDeliveredDelivered
Timers in a dedicated Web WorkerAs scheduledLightly throttled, if at allNot subject to the per minute limit in my tests

The per minute row is Chrome's "intensive throttling", introduced in Chrome 88. It applies when a page has been hidden for five minutes, has been silent for at least thirty seconds, and the timer is part of a chain of nested timeouts. A game that plays audio, even quietly, usually avoids it, which is one reason some browser games keep a near silent loop running. I would not rely on that trick, since browsers are free to change the conditions.

Why games break in three different ways

The heartbeat goes quiet

Our card game sent a heartbeat from the client every five seconds using setInterval, and the server dropped any session silent for thirty. In a hidden tab, the interval slowed to once per second at worst, which was fine. After five minutes hidden, intensive throttling pushed it to once per minute, and the server dropped the player well before the next beat. That matched the 17 percent almost exactly.

The loop stops, the messages do not

Because requestAnimationFrame pauses, any game whose logic runs inside the render loop stops processing state. WebSocket messages, however, keep arriving and queue up. When the player returns, a naive client tries to apply two minutes of updates in a single frame. You see this as a sudden fast forward, or a freeze followed by a jump, and in physics heavy games as objects flying through walls.

Time measured in ticks, not clock time

Games that count frames to measure time, "60 ticks equals one second", lose all sense of time while hidden, because no ticks happen. A cooldown that should have finished during a three minute absence is still running when the player comes back.

The fix is mostly on the server

The common advice is to "keep the tab alive" on the client with workers, audio or clever timer tricks. Some of those work today. I prefer to assume the client will be throttled and make the server comfortable with that, because a design that tolerates sleep also tolerates flaky mobile networks, laptops that close and every future browser change.

For the card game, we changed three things. The server, not the client, now sends a ping every ten seconds, and the client answers from inside its WebSocket onmessage handler. Network events are not throttled the way timers are, so the reply goes out promptly even in a hidden tab. We raised the idle timeout to ninety seconds and added a "reconnect into the same seat" path for anyone who misses it. And the server marks players as "away" when the client reports a visibilitychange to hidden, so the other players at the table see an honest status instead of a frozen opponent. Disconnects caused by hidden tabs dropped to under 2 percent of the total within a fortnight.

Design for the tab being asleep. Then a tab that stays awake is a bonus, not a requirement.

Client changes that still matter

The client cannot avoid throttling, but it can come back gracefully. Listen for visibilitychange and record the time the page went hidden. When it becomes visible again, compare against performance.now() and decide: if the gap is short, apply queued updates in order across a few frames; if it is long, throw the queue away and ask the server for a fresh snapshot of the game state. Snapshots are cheaper than replaying minutes of deltas, and they avoid the fast forward effect completely.

Measure all game time with clock readings rather than counted frames, so cooldowns and timers stay honest across a pause. And if you have logic that genuinely must keep running while hidden, such as a turn timer the player can see when they return, a dedicated Web Worker is the most reliable place for it in my experience, with the caveat that I have mostly tested desktop browsers.

For the wider picture of keeping sessions up on bad connections, see how developers keep game sessions alive through flaky connections. The messaging side is covered in how WebSockets keep game chat flowing.

Something to check this week

Open your game, start a session, switch to another tab, and set a timer for six minutes. Come back and look at two things: whether the server still considers you connected, and what the first second after return looks like. Then search your client code for heartbeats sent from setInterval. If you find one, move the responsibility to the server and answer pings from the message handler. More notes on keeping game servers calm live in our back-end section.

KO
Kofi Oosterhuis

Kofi spent years keeping APIs and game servers alive through traffic spikes and the occasional bad deploy. He covers back-end design, scaling and networking, with a preference for boring systems that do not page anyone at night.

More posts by Kofi

More in Games

01doorganico 084goad 3hot8 4465456 after5internprogram ahandfulofblessings ahmadpc ahomcybersecurity allodialresources annodex antiquesandperiodcollectibles aotc asianworldkashmir beedlectrics bloggertemplatefree bluefeatheragency bodyweightsolution campaign08blog carfixadvisor catstreetcollective cbstvsports cnfanspurchasing committeeof500yearsofdignityandresistance covertopsdjs cs-kaiyuangame cuestacomunicaciontotal cxoutlet dancebetgame deskgearhub destinyvillastay detectmagazine di33 diamondsteelbuildings donmo dyanadropship emarsysfinance emimattl escapechan eskitarih evautoplay everythingweddings-orlando farmzone feedviewerapp foxstudiosscratch gamevui123 gleasongrade greetingsday hacklabasu haskell iw-kaiyun jagoanproperti jilidskripsi joltbusinessfinance juegodepokergratis juegospucca kaeru-festival latestmehndidesigns lestanzediottaviano litemagazines localcommunityhero lustrsazan mariashebets menswheels newwebstechs1 nikkit ootheday oxigenvpn plurfitness prefetial probudgetcarpetcleaning psychicmrsmartin pyaten qvantor rejoignezlogis rexzbot riselayouts ruchin rwxqsgd5 sakaltime shibiaowang shop-bot skrtips strcoding tetapatrium topselller uneplanteartificielle vipertechcr wo-kygaming yamaha-mekar yeknava yoybuysheets zenunwa yamaha-mekar jilidskripsi ankertoto ankertoto ankertoto ankertoto