macOS: WKWebView renderer is suspended after ~16 min in background and fails to recover under memory pressure — window becomes a dead shell, force-quit required
Summary
On macOS, if OpenWorker sits in the background for ~16 minutes, WebKit suspends the WebContent (renderer) process and swaps it out. Returning normally resumes it within a second — but when the machine is under memory pressure the renderer never comes back, and the window is left as an unresponsive shell (beachball, clicks ignored). Only force-quit + relaunch recovers, which kills any running session.
Not a crash, and openworker-server stays healthy (1 ms /v1/health, no 5xx) throughout —
so this is invisible in sidecar logs.
Environment
- OpenWorker 0.2.1 (packaged
.app,com.openworker.desktop), Tauri + WKWebView - macOS 14.8.4, 8 GB RAM, uptime 19 days
Root cause (app's own unified log)
Loss of focus releases WebKit's foreground assertion (flips 1:1 with app activity):
WebPageProxy::updateThrottleState: UIProcess is releasing a foreground assertion
because the view is no longer visible
WebProcessCache::setApplicationIsActive: (isActive=0)~16 minutes later WebKit suspends the renderer — a fixed timer, measured twice the same day at 16 min 03 s and 16 min 04 s:
ProcessThrottler::dropNearSuspendedAssertionTimerFired: Removing near-suspended process assertion
WebProcessProxy::didChangeThrottleState(Suspended)The difference is memory, not the suspension
Recovers (memory headroom): after quitting Chrome (swap 4.9 GB → 0.5 GB, free pages
48 MB → 567 MB), three suspensions in one afternoon all recovered, two of them spanning
full system sleep — dropNear… at 13:14 → Sleep → ProcessDidResume 6 s after wake;
again 14:05 → Sleep → resume 6 s after wake. Same process, never restarted, window usable.
Never recovers (memory exhausted): swap 4,887/6,144 MB, ~48 MB free, ~2.5 GB
compressed. After Received memory pressure event 2 vm pressure 0, the suspended renderer
was never paged back in: 9 clicks over ~2 minutes produced only setApplicationIsActive
flapping and no ProcessDidResume; a snapshot during the freeze shows the renderer
alive with RSS ~2 MB (fully swapped out). The Rust main thread was idle in
mach_msg2_trap — no deadlock, the shell is the renderer.
Ruled out
- Not a crash — all 15 exits in 3 days were user Cmd+Q; no crash reports.
- Not the sidecar — polled normally right up to the suspension instant.
- Not sleep, not occlusion — reproduced with no sleep events; WebKit keys off app activity, not whether the view is covered.
- Keepalive doesn't help — 10 s polling + live WebSockets, renderer still suspended.
NSAppSleepDisableddoesn't help — suspensions still occurred at the same cadence; the afternoon that worked fine had it unset.
Related
The same suspension signature hit Safari, a WebKit web app, and clash-verge (also Tauri). So it's a Tauri/wry-class issue; what makes it severe here is that OpenWorker's window is the only entry point and, unlike Safari, the view is not rebuilt afterwards.
What we're asking for
- Don't suspend the renderer while work is in flight. Hold an activity assertion
(
NSProcessInfo -beginActivityWithOptions:) for a running session, or expose the equivalent of Electron'sbackgroundThrottling: falsefor the WKWebView — we understandwrymay not expose this today; that's the gap. - Recover after suspension instead of leaving a dead shell. Safari rebuilds/reloads the web content process and the user notices nothing. At minimum, re-establish the WebSocket and re-fetch state on renderer suspension/termination.
- Don't mark the app as automatically terminable.
_kLSApplicationWouldBeTerminatedByTALis set to 1 on every focus loss — a latent data-loss risk for an app running long tasks.
Repro
- On an 8 GB Mac, create memory pressure (Chrome with ~15 tabs open alongside OpenWorker): swap ≳ 4 GB, free pages < 100 MB.
- Start a session, switch away for > 16 minutes, then return and click — unresponsive
shell, no
ProcessDidResume, renderer RSS stuck ~2 MB. Force-quit is the only way out. - Control: free memory, repeat — the same suspension now recovers in ~1 s.
Full unified-log capture, main-thread sample, and process/memory snapshots at the moment
of the freeze are available on request.
Source: andrewyng/openworker