Is there an existing issue for this?
Which plugins are affected?
Database
Which platforms are affected?
Android, iOS
Description
DatabaseReference.runTransaction(updater, applyLocally: false) no longer suppresses the local-cache onValue emit in firebase_database 12.1.0+. The flag is documented to deliver only the server-confirmed value once the transaction has been finalized, but on 12.1.0+ the listener now also receives a local-estimate emit immediately, producing two onValue callbacks per write whenever the payload contains a server-resolved field such as ServerValue.timestamp.
Bisected to 12.1.0 — the Pigeon + iOS Swift / Android Kotlin rewrite shipped in #17686. 12.0.1 (last release before the rewrite) honors the flag correctly. The bug persists through 12.3.0 (latest at time of filing).
Test matrix
| firebase_database |
applyLocally |
onValue events per write |
| 12.0.1 |
false |
1 ✓ correct |
| 12.0.1 |
true (default) |
2 (local + server, expected) |
| 12.1.0 |
false |
2 ✗ regression |
| 12.1.0 |
true (default) |
2 |
Steps to reproduce
flutter create repro && cd repro
- Add
firebase_core and firebase_database: 12.1.0 to pubspec.yaml.
flutterfire configure against a test Firebase project that has a Realtime Database.
- Make sure RTDB rules at the test path allow read and write, e.g.
{ "rules": { ".read": true, ".write": true } }.
- Replace
lib/main.dart with the Sample code below. Update _databaseUrl and _path to match your test project.
flutter run on iOS or Android.
- Tap the button several times. Each tap → 2
onValue events.
- Pin
firebase_database: 12.0.1 in pubspec.yaml, flutter pub get, full restart (not hot reload — native plugin must reload), tap again. Each tap → 1 onValue event.
Expected behavior (and what 12.0.1 does)
One onValue event per write, carrying the server-confirmed value:
onValue: 2026-05-03T21:33:39.340054 value={n: 1, ts: 1777869219321}
onValue: 2026-05-03T21:33:40.555765 value={n: 2, ts: 1777869220536}
onValue: 2026-05-03T21:33:41.211048 value={n: 3, ts: 1777869221187}
Actual behavior (12.1.0+)
Two onValue events per write, ~50-70 ms apart, differing only in the ts field — first emit has the local-clock estimate, second has the server-resolved time. applyLocally: false is being ignored:
onValue: 2026-05-03T21:53:05.334006 value={n: 1, ts: 1777870385317}
onValue: 2026-05-03T21:53:05.405591 value={n: 1, ts: 1777870385387} ← duplicate
onValue: 2026-05-03T21:53:06.440080 value={n: 2, ts: 1777870386423}
onValue: 2026-05-03T21:53:06.512202 value={n: 2, ts: 1777870386492} ← duplicate
onValue: 2026-05-03T21:53:07.054799 value={n: 3, ts: 1777870387038}
onValue: 2026-05-03T21:53:07.127411 value={n: 3, ts: 1777870387108} ← duplicate
Sample code
import 'dart:async';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart'; // generated by `flutterfire configure`
// TODO: replace with your test RTDB URL and a path you can read/write.
const _databaseUrl = 'https://YOUR-INSTANCE.firebaseio.com';
const _path = 'repro/double_emit';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const _ReproApp());
}
class _ReproApp extends StatefulWidget {
const _ReproApp();
@override
State<_ReproApp> createState() => _ReproAppState();
}
class _ReproAppState extends State<_ReproApp> {
late final DatabaseReference _ref;
StreamSubscription<DatabaseEvent>? _sub;
final _events = <String>[];
int _writeCounter = 0;
@override
void initState() {
super.initState();
final db = FirebaseDatabase.instanceFor(
app: Firebase.app(),
databaseURL: _databaseUrl,
);
_ref = db.ref(_path);
_sub = _ref.onValue.listen((event) {
final line =
'${DateTime.now().toIso8601String()} value=${event.snapshot.value}';
debugPrint('onValue: $line');
setState(() => _events.insert(0, line));
});
}
@override
void dispose() {
_sub?.cancel();
super.dispose();
}
Future<void> _write() async {
_writeCounter++;
await _ref.runTransaction(
(_) => Transaction.success({
'n': _writeCounter,
'ts': ServerValue.timestamp,
}),
applyLocally: false,
);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('runTransaction(applyLocally:false) regression'),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: _write,
child: const Text(
'runTransaction(applyLocally:false) + ServerValue.timestamp',
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text('onValue events received: ${_events.length}'),
),
const Divider(height: 1),
Expanded(
child: ListView.builder(
itemCount: _events.length,
itemBuilder: (_, i) => ListTile(
dense: true,
title: Text(_events[i]),
),
),
),
],
),
),
);
}
}
Environment
firebase_database: 12.1.0 (broken) / 12.0.1 (works) — bug persists on 12.3.0
firebase_core: 4.7.0
- Flutter 3.41.5 (stable channel)
- Dart 3.11.3
- Verified on iOS (physical device + simulator) and Android
Related
Is there an existing issue for this?
Which plugins are affected?
Database
Which platforms are affected?
Android, iOS
Description
DatabaseReference.runTransaction(updater, applyLocally: false)no longer suppresses the local-cacheonValueemit infirebase_database12.1.0+. The flag is documented to deliver only the server-confirmed value once the transaction has been finalized, but on 12.1.0+ the listener now also receives a local-estimate emit immediately, producing twoonValuecallbacks per write whenever the payload contains a server-resolved field such asServerValue.timestamp.Bisected to 12.1.0 — the Pigeon + iOS Swift / Android Kotlin rewrite shipped in #17686. 12.0.1 (last release before the rewrite) honors the flag correctly. The bug persists through 12.3.0 (latest at time of filing).
Test matrix
applyLocallyonValueevents per writefalsetrue(default)falsetrue(default)Steps to reproduce
flutter create repro && cd reprofirebase_coreandfirebase_database: 12.1.0topubspec.yaml.flutterfire configureagainst a test Firebase project that has a Realtime Database.{ "rules": { ".read": true, ".write": true } }.lib/main.dartwith the Sample code below. Update_databaseUrland_pathto match your test project.flutter runon iOS or Android.onValueevents.firebase_database: 12.0.1inpubspec.yaml,flutter pub get, full restart (not hot reload — native plugin must reload), tap again. Each tap → 1onValueevent.Expected behavior (and what 12.0.1 does)
One
onValueevent per write, carrying the server-confirmed value:Actual behavior (12.1.0+)
Two
onValueevents per write, ~50-70 ms apart, differing only in thetsfield — first emit has the local-clock estimate, second has the server-resolved time.applyLocally: falseis being ignored:Sample code
Environment
firebase_database: 12.1.0 (broken) / 12.0.1 (works) — bug persists on 12.3.0firebase_core: 4.7.0Related