TLDR: If your app is multi-page and you fire a long operation as an inline fetch, navigation kills it. Move the work into a server-side background job and have the client poll for results.
The Setup
Apollo Dashboard (my personal cockpit — a local Python server that surfaces today's tasks, email triage, and AI-ranked priorities in one place) has a re-rank feature: it fires an LLM call to score and sort everything on my Today list, plays an ambient pad while it thinks, then chimes when it's done.
I love it. When it works.
The Symptom
Every time I clicked from one page to another — Tasks → Email, Email → Home, anywhere — the ambient pad cut out.
And the re-rank? Gone. No results, no chime. Like it had never started.
My First (Wrong) Theory
My first instinct was browser tab throttling. Maybe the audio context gets suspended when the tab loses focus? Maybe Chrome is throttling the fetch?
I chased that for longer than I should admit.
Apollo stopped me: "Tab over to another screen" is ambiguous. Does that mean tab switching? Or navigating to a different page?
I pinned the repro. It was page navigation. Completely different beast.
The Real Cause
Apollo Dashboard is a multi-page app. Not a SPA (single-page app). Every sidebar link is a full href — a complete page load.
That means every navigation tears down the document entirely. The in-flight POST /api/rerank fetch? Aborted. The AudioContext (my Web Audio synth in static/sfx.js)? Dead with the page. It wasn't throttling. The browser was doing exactly what browsers do when you navigate away — it killed everything.
The Fix That Actually Worked
I moved the re-rank work off the request entirely.
New file: apollo/rerank_job.py — an idempotent background daemon with a lock dict and a status tracker. Now POST /api/rerank returns in ~8ms and immediately spawns a daemon thread running today_payload(rerank=True). The results get written to .rank_cache.json on disk. A new GET /api/rerank-status endpoint returns {running, rev, error}.
The client — app.js — polls that status endpoint on every page load. Wherever you land, it hydrates from the cache. The chime fires once per completed run via sessionStorage rev-tracking, so you don't get triple-chimed for opening three pages.
The re-rank now survives navigation because the server survives navigation. The browser leaving doesn't matter — the work keeps running in Python.
Why This Matters
This is the pattern I keep relearning: if your work outlives the request, put it on the server, not in the fetch.
It only works here because Apollo Dashboard runs on a persistent local server — Python process, binds 127.0.0.1, lives as long as my machine does. The daemon thread has somewhere to run.
On Vercel or any serverless platform? Different story. Server-side fire-and-forget gets killed the moment the function returns. There you need a real queue — Inngest, a background worker, something durable. Don't let "background job" trick you into thinking it works everywhere.
Here it does. And now my dashboard remembers what it was doing.
P.S. If you're on serverless and hit this same pattern, look at Inngest — it's what I reach for on Next.js projects when I need multi-step durable execution that survives a cold function boundary.