Repeated execution of Firebase Realtime Database transaction function causing duplicate writes
0 reputation · 15 Feb 2022, 04:32 UTC
0 reputation · 15 Feb 2022, 04:32 UTC
Ensure that side‑effects inside a Firebase Realtime Database transaction execute only once despite the SDK’s automatic retry mechanism.
The SDK may invoke the transaction function up to 25 times when a conflict is detected, and it does not expose a retry count or provide built‑in deduplication, leaving developers to decide whether to make the function idempotent or move side‑effects outside the transaction.
What mechanism could Firebase provide to let applications detect retry attempts without exposing internal state? Should the SDK guarantee exactly‑once execution for transaction functions, and what trade‑offs would that entail? How can developers safely structure transaction functions to avoid duplicate side‑effects while relying on automatic retries?
29275 reputation · 15 Feb 2022, 13:33 UTC
To avoid duplicate writes or side-effects in a Firebase Realtime Database transaction, you must move all non-idempotent logic outside the transaction function. The transaction function should be treated as a pure data transformation that only returns the new value to be written to the database.
Firebase uses an optimistic concurrency control mechanism. When the SDK attempts to commit a transaction, it checks if the data has changed since it was last read. If a conflict is detected (another client updated the data), the SDK automatically re-runs the transaction function with the updated data. This process can repeat up to 25 times. Because the SDK does not provide a retry counter or a "commit-only" hook, any code inside the function—such as API calls, analytics events, or local state mutations—will execute every time a retry occurs.
The only reliable way to ensure a side-effect happens exactly once is to trigger it only after the transaction promise resolves successfully.
// ❌ UNSAFE: Side-effect inside the transaction
db.ref('counters/clicks').transaction((currentValue) => {
sendAnalyticsEvent('clicked'); // This may run 25 times
return (currentValue || 0) + 1;
});
// ✅ SAFE: Side-effect after successful commit
db.ref('counters/clicks').transaction((currentValue) => {
return (currentValue || 0) + 1;
}).then((result) => {
if (result.committed) {
sendAnalyticsEvent('clicked'); // Runs exactly once upon success
}
});
If the SDK were to guarantee exactly-once execution for the function body, it would require a distributed lock or a pessimistic locking mechanism on the server. This would significantly increase latency, reduce throughput for highly contested keys, and potentially lead to deadlocks, contradicting the high-performance, real-time nature of the database.
console.log('Transaction Attempt') inside the function. If you see multiple logs for a single successful update, the SDK is retrying.Diagnostic Detail Needed: Are you performing side-effects that depend on the specific value calculated during the transaction? If so, you should capture that value from the result.snapshot.val() in the .then() block rather than inside the transaction function.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.