Skip to content

Force Firestore long-polling to fix 30-50s stalls in Safari - #1467

Draft
codementum wants to merge 1 commit into
revisit-studies:mainfrom
codementum:fix/firestore-safari-long-polling
Draft

codementum wants to merge 1 commit into
revisit-studies:mainfrom
codementum:fix/firestore-safari-long-polling

Conversation

@codementum

@codementum codementum commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Lane here-- identified this issue and possible fix w/ Claude. Appears to be working well now.


Problem

Firestore's WebChannel transport stalls in Safari, adding 30-50s to study loads and hanging the end-of-study upload for exactly 30s. This affects any study using the Firebase storage engine.

By default the SDK holds the backchannel GET open in case the backend has more data to send. Safari buffers that response rather than delivering it incrementally, so listener data and write acknowledgements only reach the SDK when the request eventually times out.

Evidence

HAR capture from a deployed study. Safari 26.6 / macOS, firebase 11.10.0, study repo v2.4.3.

Cold study load: 57s. All 37 static assets finished in 250ms — the app shell was on the wire almost immediately. The remaining ~56s was three gaps with zero network activity:

gap duration
t+2.4s → t+31.8s 29.3s
t+31.8s → t+39.8s 7.9s
t+40.1s → t+53.5s 13.3s

introduction.md — the first asset a participant actually sees — was not requested until t+53.5s. Throughout, exactly one request was slow, and it spanned the gaps:

t+1699ms   dur=51,789ms   Listen/channel?RID=rpc&CI=0&TYPE=xmlhttp

End-of-study upload. Fifteen requests, every one 40-300ms except:

t+   98ms   dur=     44ms   Write/channel?RID=58501      <- write dispatched
t+  202ms   dur= 30,066ms   Write/channel?RID=rpc&CI=1   <- ack blocked exactly 30s
t+30107ms   dur=     43ms   ...everything resumes normally

A 44ms write whose acknowledgement takes 30.066s — a timeout, not transfer time. Participants hit the retry loop in StudyEnd.utils.ts (3 attempts, 2s/5s/10s backoff) and can end up seeing "We could not confirm your upload after 3 attempts."

The same study in Chrome is unaffected, consistent with Chrome delivering the stream incrementally.

Fix

experimentalForceLongPolling: true closes the request as soon as the backend sends data. Cost is one extra round trip per server message.

FirebaseStorageEngine currently calls initializeFirestore(firebaseApp, {}) with an empty settings object, so no transport options are set at all. Note experimentalAutoDetectLongPolling is enabled by default in firebase v11 and did not prevent this; the two settings cannot be combined.

Alternative considered

Forcing long-polling only for WebKit would preserve streaming on Chromium. I didn't browser-sniff — for a platform where participants arrive on whatever browser they own, predictable seemed better than optimal. Happy to narrow it if you'd prefer.

Testing

Verified against a 36-step survey (markdown/image/form components, Firebase engine) in Safari: the 30-50s load stalls and the 30s end-screen hang are both gone. eslint clean, and tsc reports no errors in the changed file.

I have not tested this against studies using realtime listeners heavily (e.g. the live monitor view), where the extra round trip per message may be more noticeable.

The WebChannel backchannel is held open by default in case the backend has more
data to send. Safari buffers that response rather than delivering it
incrementally, so listener data and write acknowledgements only reach the SDK
when the request eventually times out.

Measured on a deployed study in Safari 26.6: a cold load took 57s, of which
~50s was three gaps with zero network activity while a single backchannel sat
open for 51.8s. All 37 static assets had finished in 250ms, and the first
participant-visible asset was not requested until t+53.5s. The end-of-study
upload shows the same shape -- the write dispatches in 44ms, its acknowledgement
blocks for exactly 30.066s -- which participants see as "We could not confirm
your upload after 3 attempts."

The same study in Chrome is unaffected, consistent with Chrome delivering the
stream incrementally.

experimentalAutoDetectLongPolling is enabled by default in firebase v11 and does
not prevent this. experimentalForceLongPolling closes the request as soon as the
backend sends data, costing one extra round trip per message. The two settings
cannot be combined.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codementum
codementum requested a review from JackWilb September 11, 2026 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant