Firestore Offline Persistence: Local Writes, Rule Gaps & Conflict Wins
Firestore caches writes locally offline and replays on reconnect. Rule changes may silently reject them. Learn conflict-resolution defaults, onConflict setup, and verify offline with debug logs.
04 Feb 2026, 07:59 UTC

Enable Firestore offline persistence and the app stops asking if the network is up? But the assumption that locally accepted writes always survive reconnection is where things quietly break. When a write lands in the persistent local queue while the device is offline, it replays automatically when connectivity returns—unless security rules have changed or a conflict reshapes the document on the server. This blog walks through the concrete behavior, one supported configuration, and how to verify what’s really happening beneath the SDK.
Local caching and automatic replay
Firestore can cache writes in a persistent queue per collection. Enable it once with FirestoreSettings.setPersistenceEnabled(true) (or the declarative firebase.firestore().settings({ persistenceEnabled: true })) and the SDK stores acknowledged mutations locally, on disk, associated with the current Firebase installation. When the network returns, the queue replays each write in order, using server-generated timestamps to resolve any overlap. The key takeaway: the replay is automatic and, for operations that the server originally accepted, does not cause data loss. However, the cache is tied to the device and installation; clearing app data or reinstalling resets the entire cache and any unwritten local changes.
- Persistence is per-collection, not per-database, and survives app restarts on the same device.
- Writes that were still pending when the network vanished stay in the queue; once reconnected, they are retried automatically.
- Security rules are evaluated at read time, even for documents served from the offline cache.
Security rules and the silent reconnect failure
Because security rules are re-evaluated every time a document is read—including from the offline cache—a write that was accepted locally can be rejected by the server if rules change before reconnection. The client does not surface an error during the offline window; the failure surfaces when the write queue replays and the server returns a permission denied response. This silent gap is the most common source of missing writes after an offline period, especially in apps where rules are updated frequently or where different user roles have divergent permission sets.
// Enable persistence at app startup
import { Firestore } from 'firebase/firestore';
const db = getFirestore();
const settings = new FirestoreSettings();
settings.setPersistenceEnabled(true);
db.configureSettings(settings);After reconnection, compare the local write counter with the server write counter (see the verification section) to confirm whether any writes were silently rejected.
Conflict resolution and the onConflict option
When two writes target the same document—one from the local queue and one from a concurrent server update—Firestore resolves the conflict using last-write-wins timestamps. The SDK exposes an onConflict option that influences this behavior. Setting it to 'merge' attempts a three-way merge using the two client writes and the server's prior version; if the merge cannot be resolved automatically, the operation fails and you can catch the error to apply your own strategy. Omitting the option defaults to last-write-wins. This gives you a way to preserve incremental updates, such as array append or field-level increments, without losing the latest server-provided value.
// Example: prefer a merge strategy when replaying local writes
db.collection('metrics').doc('daily-2024-09-01').set(
{ visits: increment(1) },
{ merge: true, onConflict: 'merge' }
);If your app requires deterministic merges (e.g., counters, arrays), implement the merge client-side before writing, then use { merge: true } to push the pre-merged data.
Worked example: enabling, running, and verifying offline writes
Let’s walk through a minimal setup that enables persistence, performs a write while offline, and verifies the replay behavior.
Step 1 – Enable persistence
// In your app initialization (Node.js, React Native, or web browser console)
import { Firestore, FirestoreSettings } from 'firebase/firestore';
const db = getFirestore();
const settings = new FirestoreSettings();
settings.setPersistenceEnabled(true);
db.configureSettings(settings);Where to run: Once, during app startup, before any Firestore operations. No special permissions beyond standard Firebase project access are required.
Step 2 – Write while offline
// Simulate offline and add a document
async function addOfflineNote() {
try {
const ref = db.collection('offline-notes').doc();
await ref.set({ text: 'test note written offline', timestamp: Date.now() });
console.log('Write queued locally (offline mode)');
} catch (e) {
console.error('Write failed even locally:', e);
}
}Meaningful placeholder: Replace offline-notes with your collection name. The set call returns immediately, placing the mutation in the persistent write queue.
Step 3 – Reconnect and verify
// Listen for the write to replay
const unsub = db.collection('offline-notes').onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => {
if (change.type === 'added' || change.type === 'modified') {
console.log('Document updated after replay:', change.doc.id, change.doc.data());
}
});
});Expected check: You should see the document appear (or update) once the network is available. If the write was rejected by a rule change, the document will not appear and the local write counter in debug logs will differ from the server counter.
Step 4 – Enable debug logging and compare counters
// Enable verbose logging to confirm offline activity
firebase.firestore().setLogLevel(firebase.firestore.LogLevel.DEBUG);
// In log output, look for lines like:
// LOCAL_WRITE: queued for doc 'offline-notes/abc123'
// SERVER_WRITE: acknowledged for doc 'offline-notes/abc123'
// Compare the count of LOCAL_WRITE events with SERVER_WRITE events after reconnection.
// If LOCAL_WRITE > SERVER_WRITE, some writes were silently rejected (often due to rule changes).
You can also open the Firebase Console, navigate to Firestore Data profiling, select a document ID, and inspect the cached metadata versus the server version to see if a reconciliation occurred.
Limitation and practical check
Offline persistence is device- and installation-specific. If a user switches phones, uninstalls the app, or clears storage, the entire local write queue and cached documents are discarded. Under unstable network conditions, a large queued write volume can increase battery consumption as the SDK retries connectivity-dependent operations. To practical-check this, run the disconnection-reconnection sequence in a controlled environment (e.g., using Chrome’s Network Conditions throttling) and monitor the write queue size via the DEBUG logs. If the queue grows beyond a few dozen entries under flaky connectivity, consider implementing a client-side backoff or a draft-save UI that explicitly asks the user before queuing many operations.
Actionable closing
- Enable persistence only if your app can handle the reconnection flow—listen to snapshot updates and inspect debug counters after the first reconnect.
- Choose an onConflict strategy deliberately: 'lastWrite' for simple overwrite semantics, 'merge' only after you’ve verified it matches your data model, otherwise merge client-side before the write.
- Add a periodic health check that compares localWrite and serverWrite counters (via the DEBUG log level) and alerts you if the gap widens unexpectedly.
- Document that offline cache resets on app uninstall or data clearance, and design your UI to surface unsaved changes before the user leaves the app.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.