Supabase

Fix 'Failed to get document because the client is offline'

The emulator call from the Stack Overflow question, reproduced with the Node SDK against a real Firestore emulator: what the SDK builds from the host string, which other causes give the same error, and which do not.

Mahdi Ben RhoumaMahdi Ben Rhouma
7 min read

TL;DR#

Failed to get document because the client is offline is what getDoc throws when the Firestore SDK has decided it cannot reach its backend and the document is not in its local cache. In the question, the reason the backend is unreachable is the address itself: connectFirestoreEmulator(db, "https://localhost", 8080) passes a scheme where the SDK expects a bare host name. The SDK glues the host and port into its own address, so the call never reaches the emulator. Remove the scheme and keep the port separate: connectFirestoreEmulator(db, "localhost", 8080).

The Stack Overflow question comes from a Next.js course project where the Auth emulator worked and the Firestore one did not. The asker's own answer confirms the fix: removing https:// in front of localhost.

The emulator setup that fails#

The question's Firebase module connects both emulators when the page is served from localhost:

js
if (window.location.hostname === "localhost") {
  connectAuthEmulator(auth, "http://localhost:9099");
  connectFirestoreEmulator(db, "https://localhost", 8080);
}

The two calls look parallel, but they are not. connectAuthEmulator takes a URL: its source documents the parameter as "The URL at which the emulator is running (eg, 'http://localhost:9099')", so the Auth line is correct. connectFirestoreEmulator takes a host and a port as separate arguments; its source documents the parameter as "the emulator host (ex: localhost)". Copying the Auth line's shape into the Firestore line is how the scheme ends up in the wrong place.

The error was reproduced on 10 October 2026, with firebase 13.0.0 on Node 22 in one container and the Firestore emulator from firebase-tools 15.33.0 (demo project, no rules file) in another, reachable under the host name emulator. With the scheme in the host argument, the read fails at once:

text
firebase 13.0.0 | connectFirestoreEmulator(db, "https://emulator", 8080)
[2026-10-10T10:04:18.033Z]  @firebase/firestore: Firestore (13.0.0): GrpcConnection RPC 'Listen' stream 0x2473f309 error. Code: 14 Message: 14 UNAVAILABLE: Name resolution failed for target dns:https://emulator:8080
[2026-10-10T10:04:18.034Z]  @firebase/firestore: Firestore (13.0.0): Could not reach Cloud Firestore backend. Connection failed 1 times. Most recent error: FirebaseError: [code=unavailable]: 14 UNAVAILABLE: Name resolution failed for target dns:https://emulator:8080
This typically indicates that your device does not have a healthy Internet connection at the moment. The client will operate in offline mode until it is able to successfully connect to the backend.
getDoc rejected after 0.1s: FirebaseError code=unavailable message=Failed to get document because the client is offline.

The first two lines are warnings the SDK logs to the console; the last line is the rejected promise the app sees. If the browser console shows the "Could not reach Cloud Firestore backend" warning just before your error, the connection is the problem, not the document.

Why it happens#

Two pieces of the firebase-js-sdk source explain the whole chain.

The host string is used as is. In lite-api/database.ts, connectFirestoreEmulator stores the new host setting as the template string ${host}:${port}, with SSL turned off unless the host is a Cloud Workstations address. Nothing strips a scheme. The browser build then builds its base URL in remote/rest_connection.ts as the protocol, then ://, then that host setting. With the question's arguments that gives http://https://localhost:8080. Parsed according to the WHATWG URL Standard, which Node and browsers both implement, it does not point at the emulator at all:

text
href: http://https//localhost:8080/google.firestore.v1.Firestore/Listen/channel
hostname: "https" port: "" pathname: //localhost:8080/google.firestore.v1.Firestore/Listen/channel

The host becomes https, the port becomes the default one, and localhost:8080 moves into the path. The Node build uses gRPC instead of WebChannel and hands the same host setting to gRPC, which fails on name resolution, as the run above shows. The browser transport itself was not run here; its failure is read from the source and the URL parse.

Offline is a state the SDK decides on. In remote/online_state_tracker.ts, one failed watch-stream attempt is enough to switch the client to Offline (MAX_WATCH_STREAM_FAILURES = 1), and a timer does the same when the stream neither connects nor fails (ONLINE_STATE_TIMEOUT_MS = 10 * 1000, in milliseconds). The installed firebase 13.0.0 package carries the same two constants. That is why the run above rejects after 0.1s: the name lookup failed immediately. A firewall that silently drops packets would instead make the read wait about ten seconds before failing.

The error is a cache miss while offline. In core/firestore_client.ts, getDoc listens for one snapshot of the document. If that snapshot comes from the cache and does not contain the document, the SDK rejects with code unavailable and this exact message, because it cannot tell a missing document from one it has never downloaded. A source comment in that function says so: the promise is rejected only when offline.

Fix#

  1. Pass a bare host and a separate port. The Firebase emulator documentation and its official snippet pass the loopback IP address as the host and the port number on its own. The asker reports that localhost without the scheme fixed their app, which gives:

    js
    if (window.location.hostname === "localhost") {
      connectAuthEmulator(auth, "http://localhost:9099");
      connectFirestoreEmulator(db, "localhost", 8080);
    }
  2. Match the host and port to the ones the emulator prints. When firebase emulators:start reports that Firestore is ready, it prints a table with the host and port it listens on. The address in connectFirestoreEmulator must reach that one. If you changed emulators.firestore.port in firebase.json, change the call too.

  3. Restart the dev server and reload the page. The SDK's own documentation for connectFirestoreEmulator says it must be called before the instance has been used for any operation, so start from a fresh page rather than a hot-reloaded one. If a reload re-runs your Firebase module against an instance that is already in use with different emulator settings (or with any settings on older SDK releases), the SDK throws a different error, covered in Fix Firestore 'Host has been set in both settings()' error, which also shows a connect-once guard that survives reloads.

Verify the fix#

Run the same read with only the scheme removed. Against an emulator that has never received the document, the promise now resolves and reports that the document is absent, with no warning in the console:

text
firebase 13.0.0 | connectFirestoreEmulator(db, "emulator", 8080)
getDoc resolved after 0.8s: exists() = false 

After writing the document first, the same call returns its data:

text
seeded usernames/alice
getDoc resolved after 0.2s: exists() = true {"uid":"u1"}

In the browser, the Network tab should now show Firestore requests going to localhost:8080, and the "Could not reach Cloud Firestore backend" warning should no longer appear in the console.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

Causes that give the same message, and one that does not#

The top answer on the question lists several possible causes. The run checked the two that are testable against an emulator.

A wrong port does give the same error. With a bare host but a port where nothing listens, the stream is refused and getDoc fails exactly like the scheme case:

text
firebase 13.0.0 | connectFirestoreEmulator(db, "emulator", 8081)
getDoc rejected after 0.1s: FirebaseError code=unavailable message=Failed to get document because the client is offline.

So the advice to "try 8081" only helps if your emulator actually listens on 8081. The port to use is the one in the emulator's startup table, not a guess.

A missing document or collection does not. The answer also suggests the collection might not exist yet. When the client is online, a missing document is a normal result, exists() returning false, as shown in the verification above. If you see the offline error, a missing collection is not the cause.

Offline persistence does not fix a client that never connected. The offline data documentation describes persistence as a cache of data the app is actively using. With the wrong address, nothing is ever fetched, so there is nothing to cache. Persistence helps a real user who loses their connection after reading a document, not a misconfigured emulator.

The second answer on the question deletes and recreates a database, but its code calls getDatabase, which belongs to the Realtime Database, a different product with its own client. If you are unsure which of the two your project uses, Cloud Firestore vs Realtime Database in 2026 sets out the differences.

When the connection works but the read is refused#

Once the client reaches the emulator, security rules start to apply. The emulator logs, when it starts without a rules file, that it "will default to allowing all reads and writes". With a deny-all rules file loaded, the same getDoc on a correctly connected client fails with a different code:

text
getDoc rejected after 1.0s: FirebaseError code=permission-denied message=
false for 'get' @ L5

That is progress: the error has moved from the connection to your rules. If you now see permission-denied or Missing or insufficient permissions, continue with Firestore PERMISSION_DENIED: Every Real Cause and Fix.

In short, this message is the SDK saying it is offline and has nothing cached, not that your data is missing. With the emulator, check the host argument first: a bare localhost, the port from the emulator's own startup table, and no scheme. Once the console stops printing the "Could not reach Cloud Firestore backend" warning, any error that remains is about the data or the rules, not the connection.

Further reading: Firebase: Cannot read property 'getProvider' of undefined. Also see Firebase Google Sign-in on Vercel: Custom authDomain Fix.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote