firebase / firebase/firebase-ios-sdk

Firestore: unacked writes replayed on reconnect, double-applying FieldValue.increment (see firebase-android-sdk#8440)

Open
#16,475 1 comment 0 reactions 1 assignee Claimed by @MarkDuckworth View on GitHub
api: firestore
Dominant language
C++
Stars
6.7k
Forks
1.8k
Avg merge
2d 18h
Merged PRs (30d)
75

Description

### Description

This is a cross-SDK tracker for a Firestore defect that has already been confirmed and root-caused on Android in firebase/firebase-android-sdk#8440. The cause described there is not Android-specific, so I'm filing here so the iOS SDK isn't missed when it's addressed.

Root cause, as described by the Android SDK team ([comment](https://github.com/firebase/firebase-android-sdk/issues/8440#issuecomment-5122923991)):

1. The backend (`/google.firestore.v1.Firestore/Write`) does not currently support write stream resumption across connections (`stream_token`).
2. If a connection drops after the server commits a write batch but before the ACK reaches the client, the client re-sends the unacknowledged batch on reconnect, and the server — unable to resume the previous stream session — executes it a second time.
3. Replaying an idempotent write (e.g. `set(v: 5)`) is harmless, but replaying a relative transform like `FieldValue.increment(-1)` applies the delta twice. Nothing fails: the completion handler reports success while client and server silently diverge.

Since the trigger is a backend limitation combined with the ordinary client behavior of re-sending unacknowledged batches, any client SDK that queues offline mutations should be affected — this one included.

**Expected:** each queued mutation is applied exactly once when connectivity returns.

**Actual:** a batch that was committed but not acknowledged before a disconnect is applied twice; `increment` counters drift by one delta per occurrence, with no error surfaced.

### Reproducing the issue

The original report ([firebase/flutterfire#12952](https://github.com/firebase/flutterfire/issues/12952)) reproduced on **both iOS and Android** through FlutterFire, which only forwards `increment` / `disableNetwork` / `enableNetwork` to the native SDKs. A FlutterFire maintainer then reduced it to a [pure native Android project](https://github.com/TarekkMA/firebase-android-reproduction/tree/ff-issue/12952), and the FlutterFire issue was closed as an upstream native SDK bug.

I don't have a pure-Swift reproduction — the iOS observation came through FlutterFire. The equivalent sequence:

```swift
let db = Firestore.firestore()
let doc = db.collection("test").document("doc") // field "v" starts at 0

doc.updateData(["v": FieldValue.increment(Int64(-1))]) // not awaited, as in real offline-capable apps
db.disableNetwork()
doc.updateData(["v": FieldValue.increment(Int64(1))])
db.enableNetwork()
```

Repeat every 2–3 seconds (or fire several sequences concurrently to make it near-deterministic) while watching the document with a snapshot listener. `v` should return to `0` after each `-1`/`+1` pair; instead it drifts negative, consistent with the `-1` batch being applied twice.

`disableNetwork()` / `enableNetwork()` are only a deterministic stand-in for a flaky real-world connection — the original report saw the same drift on a physical device with a genuinely unstable network.

### References

- Android issue with the team's root-cause analysis: firebase/firebase-android-sdk#8440
- Internal tracking for the backend write-stream-resumption / mutation-dedup work: **b/540459641** — per [this comment](https://github.com/firebase/firebase-android-sdk/issues/8440#issuecomment-5132437408), #8440 is linked to it and is the public reference to follow
- Original FlutterFire report (reproduced on iOS and Android): firebase/flutterfire#12952

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.