Skip to content

Instantly share code, notes, and snippets.

@HerrNiklasRaab
Last active July 24, 2026 18:47
Show Gist options
  • Select an option

  • Save HerrNiklasRaab/54755252afc835d4a787d6bf9e117873 to your computer and use it in GitHub Desktop.

Select an option

Save HerrNiklasRaab/54755252afc835d4a787d6bf9e117873 to your computer and use it in GitHub Desktop.
InstantDB: frozen sibling tab parks the IndexedDB kv lock, subscribeAuth blocks indefinitely (repro)

InstantDB: frozen sibling tab parks the IndexedDB kv lock and blocks client boot indefinitely

@instantdb/core boot (subscribeAuth, getAuth, first query) waits unboundedly on reading the kv object store of instant_<appId>_<v>. If any other browsing context on the same origin holds a readwrite transaction on that store, boot blocks with no timeout and no fallback. A tab that the OS process-freezes mid-transaction (Android's cached-app freezer; SIGSTOP on desktop) holds that lock indefinitely — the browser never aborts it, because the transaction still has a request in flight.

Observed in production on Android Chrome: user requests a magic link, the tab sits in background, Android freezes it mid-kv-write; user opens the link in a new tab; the new tab authenticates over the network fine but subscribeAuth never fires — endless spinner until the frozen tab is killed or thawed.

Repro (desktop Chrome, ~5 min)

Everything in the page (repro.html) uses only public APIs: init, transact, tx, id, subscribeAuth, getAuth. Any random UUID works as appId — the client creates its IndexedDB store and resolves auth locally before/without network. The freeze is simulated with SIGSTOP, which is what Android's app freezer effectively does to a cached renderer.

python3 -m http.server 8734
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --user-data-dir=/tmp/instant-repro-profile --remote-debugging-port=9223 \
  http://127.0.0.1:8734/repro.html "http://127.0.0.1:8734/repro.html?role=holder"
  1. victim tab (repro.html): logs subscribeAuth fired … boot complete in ~30 ms. Baseline.

  2. holder tab (?role=holder): an ordinary Instant client issuing transact() every 25 ms. Pending mutations are persisted to the kv object store on a ~100 ms throttle (PersistedObject, saveThrottleMs: 100). Each mutation carries a 64 KB payload so the persisted map — and with it each readwrite transaction — grows quickly; this widens the window you need to hit, it does not create it. Keep this tab focused for ~30 s so its timers aren't throttled.

  3. Find the holder's renderer PID (Chrome task manager, or SIGSTOP each renderer of the debug profile and see which tab stops responding), then:

    kill -STOP <holder-renderer-pid>

    and reload the victim tab. The stop must land while a kv save is in flight — with the fat payloads that's roughly 1 in 5 attempts (kill -CONT, wait a second, -STOP again, reload victim, repeat).

  4. Result when caught: the victim logs … subscribeAuth still hasn't fired (auth boot blocked on kv) forever. In our run it stayed blocked for 198 s with no browser-side abort, then resolved within ~1 s of kill -CONT.

What we ruled out

  • Graceful lifecycle freeze doesn't reproduce this. 30 attempts with Page.setWebLifecycleState {state: "frozen"} (the chrome://discards freeze) never parked the lock — Chrome appears to let in-flight IndexedDB transactions settle before/while freezing. The indefinite variant needs a process-level freeze, which is what Android does to cached tabs.
  • An idle frozen tab is harmless. Only a freeze landing mid-transaction parks the lock. Real tabs write kv on every pending-mutation change, auth change, etc., so over hours of background life the window gets hit — as our production incident shows.
  • If the parked transaction has no request in flight (callback queued but unexecuted), Chrome aborts it after ~60 s ("Transaction timed out due to inactivity") and the victim recovers. With a request in flight there is no timeout at all.

Environment

  • @instantdb/core 0.21.26 (pinned in repro.html via esm.sh)
  • Chrome 150.0.7871.182 (macOS, desktop repro), Chrome 150.0.7871.126 (Android 16, field incident + partial verification)

Suggested fixes / asks

  1. Boot reads of kv (auth, pendingMutations) should have a timeout + degraded path (e.g. proceed with network auth / in-memory storage and reconcile when the lock frees) instead of blocking subscribeAuth indefinitely.
  2. Expose Storage injection in @instantdb/react's init (core already accepts it; InMemoryStorage exists but isn't exported) so apps can opt out of shared IDB state where cross-tab locking is a hazard.
<!doctype html>
<meta charset="utf-8" />
<title>InstantDB kv-lock repro</title>
<body style="font-family: ui-monospace, monospace; padding: 24px; max-width: 720px">
<h2 style="margin-top: 0">InstantDB: frozen tab parks the <code>kv</code> lock and blocks sibling boot</h2>
<p>
<a href="?">victim (plain boot, measures subscribeAuth)</a> &nbsp;|&nbsp;
<a href="?role=holder">holder (normal Instant client doing transact()s)</a>
</p>
<div id="clock" style="font-size: 40px; font-weight: bold; margin: 8px 0">--:--:--</div>
<div id="log" style="white-space: pre-wrap; background: #f4f4f4; padding: 12px; border-radius: 8px"></div>
<script type="module">
// Both tabs use ONLY public @instantdb/core APIs: init, transact, tx, id,
// subscribeAuth, getAuth. The app id can be any UUID — the client creates
// its IndexedDB store (instant_<appId>_<v>) locally and resolves auth from
// it before/without network. With no reachable app, transact() mutations
// stay pending, and pending mutations are persisted to the `kv` object
// store on every change (PersistedObject, saveThrottleMs=100).
const APP_ID = "b6e8636f-2f96-4a67-8b8a-6a4bb1f3d1f0";
const logEl = document.getElementById("log");
const t0 = performance.now();
const log = (m) => {
const line = `[${(performance.now() - t0).toFixed(0).padStart(6)}ms] ${m}`;
logEl.textContent += line + "\n";
console.log(line);
};
const clockEl = document.getElementById("clock");
setInterval(() => {
const d = new Date();
clockEl.textContent = d.toLocaleTimeString("de-DE") + "." + Math.floor(d.getMilliseconds() / 100);
}, 100);
const { init, tx, id } = await import("https://esm.sh/@instantdb/core@0.21.26");
const role = new URLSearchParams(location.search).get("role");
const banner = document.createElement("div");
banner.textContent = role === "holder" ? "HOLDER TAB (gets frozen)" : "VICTIM TAB (boots the client)";
banner.style.cssText = `padding: 10px 14px; margin-bottom: 10px; border-radius: 8px; font-size: 22px; font-weight: bold; color: #fff; background: ${role === "holder" ? "#c0392b" : "#2471a3"}`;
document.body.prepend(banner);
document.title = role === "holder" ? "HOLDER" : "VICTIM";
const db = init({ appId: APP_ID });
if (role === "holder") {
// An ordinary Instant client under write load. Each transact() enqueues a
// pending mutation; the client persists the pending-mutation map to the
// kv object store (readwrite tx) on a 100ms throttle. Freeze this tab
// (chrome://discards -> Freeze, or Android tab freezing) and one of
// those transactions is left open forever: the frozen tab can't run its
// completion callbacks, so the kv lock stays parked.
let n = 0;
let lastErr = null;
const blob = "x".repeat(64 * 1024);
setInterval(() => {
try {
void db.transact(tx.todos[id()].update({ t: ++n, blob })).catch((e) => {
if (String(e) !== lastErr) { lastErr = String(e); log(`transact rejected: ${e}`); }
});
if (n % 50 === 0) log(`transact() #${n} (~${((n * 64) / 1024).toFixed(1)}MB pending in kv)`);
} catch (e) {
if (String(e) !== lastErr) { lastErr = String(e); log(`transact threw: ${e}`); }
}
}, 25);
log("holder running: transact() every 25ms -> kv readwrite tx every ~100ms");
log("freeze THIS tab, then reload the victim tab.");
} else {
let authResolved = false;
db.subscribeAuth((auth) => {
if (!authResolved) {
authResolved = true;
log(`subscribeAuth fired: user=${auth.user ? auth.user.email : "null"} <-- boot complete`);
}
});
db.getAuth().then(
(u) => log(`getAuth() resolved: ${u ? u.email : "null"}`),
(e) => log(`getAuth() rejected: ${e}`),
);
log("victim booting: waiting for subscribeAuth …");
const tick = setInterval(() => {
if (authResolved) { clearInterval(tick); return; }
log("… subscribeAuth still hasn't fired (auth boot blocked on kv)");
}, 5000);
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment