You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
supabase.rpc() calls hang indefinitely (~5–10% of calls) when createClient is configured with lock: async (_n, _t, fn) => fn() (No-Op lock for multi-tab scenarios). The Promise neither resolves nor rejects. PR #2106's steal: true retry fix doesn't apply because No-Op lock bypasses the navigator.locks code path entirely.
Environment
@supabase/supabase-js2.105.4 (latest stable as of 2026-05-08)
Observe: 1–2 of the 20 calls hang. They eventually error with our fetchWithTimeout's TimeoutError after 15s. Without the timeout wrapper, the Promise stays pending forever.
Expected behavior
All 20 RPC calls resolve with HTTP 200 within ~100ms each.
Actual behavior
5–10% of calls hang. They are not aborted by the client, they don't time out at the PostgREST layer (the request never reaches the server — see evidence below). Without an external timeout wrapper, they remain pending indefinitely.
Evidence — server is fine, client is the culprit
Equivalent direct fetch() (bypassing the SDK entirely, same auth token from localStorage['sb-<project-ref>-auth-token'], same endpoint, same headers):
#2111 describes orphaned navigator.locks leading to indefinite auth hangs. PR #2106 fixes that by retrying lock acquisition with { steal: true } after a 5s timeout.
Our config explicitly bypassesnavigator.locks via lock: async (_n, _t, fn) => fn(). The steal: true retry path is never taken because the lock callback resolves immediately. We're hitting a different race.
Hypothesis
Without lock-based serialization, the auth-refresh background cycle (or some other internal queue) can interleave with concurrent RPC requests in a way that leaves a fetch in a non-resolvable state. PostgREST client internals (auth-token refresh handler? request queue?) seem to assume the lock semantics are present.
This is consistent with:
Bug only manifests post-login (anonymous-only calls don't reproduce reliably for us).
Bug rate is ~5–10%, suggesting timing-dependent race.
Direct fetch (which bypasses the entire SDK's request pipeline) is 100% reliable.
Asks
Is lock: async (_n, _t, fn) => fn() (No-Op lock) a supported configuration? The TypeScript signature allows it, but if the SDK assumes lock semantics elsewhere, the No-Op breaks invariants.
If supported: is there a known race condition between the No-Op lock path and auth-refresh / request queueing that could leave fetches in a pending-forever state?
If not supported: what's the recommended pattern for opting out of multi-tab lock serialization without losing the SDK's request reliability? (Background: many internal-tool SPAs don't need cross-tab serialization but were hit by the original navigator.locks orphan cluster.)
Workaround currently in production
We bypass the SDK for the affected RPCs entirely (direct fetch() with manual token extraction). Plus a global fetchWithTimeout wrapper as a safety net for any other SDK call that might hang. Functional but introduces technical debt (duplicate request path, manual auth-storage parsing).
Happy to provide a minimal runnable StackBlitz repro if useful — let me know.
Filed from an internal SaaS team running supabase-js in a small React admin app — happy to dig deeper if useful.
Bug Summary
supabase.rpc()calls hang indefinitely (~5–10% of calls) whencreateClientis configured withlock: async (_n, _t, fn) => fn()(No-Op lock for multi-tab scenarios). The Promise neither resolves nor rejects. PR #2106'ssteal: trueretry fix doesn't apply because No-Op lock bypasses thenavigator.lockscode path entirely.Environment
@supabase/supabase-js2.105.4(latest stable as of 2026-05-08)@supabase/auth-js2.105.4(bundled, contains PR fix(auth): recover from orphaned navigator locks via steal fallback #2106 fix already)19.2.68.0.12(dev server, React Strict Mode off)130+on macOS14Setup (relevant
createClientconfig)Reproduction steps
supabase.auth.signInWithPassword(...).fetchWithTimeout'sTimeoutErrorafter 15s. Without the timeout wrapper, the Promise stays pending forever.Expected behavior
All 20 RPC calls resolve with HTTP 200 within ~100ms each.
Actual behavior
5–10% of calls hang. They are not aborted by the client, they don't time out at the PostgREST layer (the request never reaches the server — see evidence below). Without an external timeout wrapper, they remain pending indefinitely.
Evidence — server is fine, client is the culprit
Equivalent direct
fetch()(bypassing the SDK entirely, same auth token fromlocalStorage['sb-<project-ref>-auth-token'], same endpoint, same headers):Result on our environment: 10/10 HTTP 200, avg 93ms, max 322ms. Same RPC, same user, same session, same browser — no hangs.
Why this is NOT issue #2111 / fixed by PR #2106
#2111 describes orphaned
navigator.locksleading to indefinite auth hangs. PR #2106 fixes that by retrying lock acquisition with{ steal: true }after a 5s timeout.Our config explicitly bypasses
navigator.locksvialock: async (_n, _t, fn) => fn(). Thesteal: trueretry path is never taken because the lock callback resolves immediately. We're hitting a different race.Hypothesis
Without lock-based serialization, the auth-refresh background cycle (or some other internal queue) can interleave with concurrent RPC requests in a way that leaves a fetch in a non-resolvable state. PostgREST client internals (auth-token refresh handler? request queue?) seem to assume the lock semantics are present.
This is consistent with:
Asks
lock: async (_n, _t, fn) => fn()(No-Op lock) a supported configuration? The TypeScript signature allows it, but if the SDK assumes lock semantics elsewhere, the No-Op breaks invariants.Workaround currently in production
We bypass the SDK for the affected RPCs entirely (direct
fetch()with manual token extraction). Plus a globalfetchWithTimeoutwrapper as a safety net for any other SDK call that might hang. Functional but introduces technical debt (duplicate request path, manual auth-storage parsing).Happy to provide a minimal runnable StackBlitz repro if useful — let me know.
Filed from an internal SaaS team running supabase-js in a small React admin app — happy to dig deeper if useful.