Firenook against the official emulator
Two hours of a real application’s traffic against each emulator in turn, on one server, every operation timed by the client. Then each emulator on its own, for starts, stops, queries and a store of 2.7 million documents.
In short
Firenook against firebase-tools 15.22.0 on Java 26, measured on 21–22 October 2026. Lower is better on every row.
Memory at peak
10× less
- Official
- 22.6 GiB
- Firenook
- 2.11 GiB
Write, median
40× faster
- Official
- 208 ms
- Firenook
- 5.1 ms
Listener update, median
19× faster
- Official
- 209 ms
- Firenook
- 11 ms
Restart with data kept
52× sooner
- Official
- 30.8 s
- Firenook
- 0.59 s
orderBy, limit 20
8× faster
- Official
- 96.3 ms
- Firenook
- 12.0 ms
CPU over two hours
61% less
- Official
- 1,586 s
- Firenook
- 616 s
Memory, latency and CPU: one application, its data and the same two-hour workload, one emulator at a time. Restart and query: Firestore, Auth and Storage on the same data, no application. Every multiple is rounded down.
What ran
One server, one real application and its data, and the same two hours of traffic for each emulator.
- The server
- An AMD Ryzen 5 3600 (6 cores, 12 threads) with 64 GiB of memory and NVMe storage, Ubuntu 24.04, Node 24.20.0. One emulator at a time, official first, each from a clean start, nothing else running.
- The application
- A production web application with its API, a Functions codebase and a cache worker, on a copy of its data: 211,205 Firestore documents, 33,352 Storage objects (6.7 GB) and one Auth user. Both stacks had to end with identical data.
- The traffic
- Writes from two busy sessions, open listeners, a filtered query every 15 seconds, a Firestore-triggered function, and a Storage upload, metadata read, download and delete, plus an idle second client issuing the same reads. Latency is timed by the client, from call to answer: 5,280 writes and 60,000 listener updates per emulator, every one delivered.
The run passed every check: both soaks with exactly the same operations and no error, stall or lost update, 36 of 36 browser journeys, identical data after every stage, and 95 evidence files re-verified by an independent audit.
Two hours of an application’s traffic
Firenook was faster at the median and at the 99th percentile on every row.
| Median / 99th percentile | Official | Firenook | Median |
|---|---|---|---|
| Write, acknowledged | 208.2 / 279.8 ms | 5.1 / 50.6 ms | 40× faster |
| Listener update after a write | 209 / 258 ms | 11 / 60 ms | 19× faster |
| Filtered query under load | 33.1 / 44.1 ms | 6.6 / 31.0 ms | 5.0× faster |
| The same query from an idle client | 32.6 / 43.8 ms | 7.1 / 30.9 ms | 4.5× faster |
| Firestore-triggered function | 3.80 / 8.02 ms | 3.11 / 4.49 ms | 1.2× faster |
| Storage upload, metadata, download, delete | 17.8 / 26.8 ms | 8.6 / 11.8 ms | 2.0× faster |
Within the Storage cycle, downloads were the one step where Firenook was not faster: 2.30 / 5.01 ms against 2.12 / 4.31 ms.
Memory, CPU and disk
The official emulator keeps all data in its Java heap, which grew for the whole run. Firenook keeps data on disk and caches what it reads.
| Over the two hours | Official | Firenook |
|---|---|---|
| Emulator memory, peak (PSS) | 22.57 GiB (Java) | 2.11 GiB |
| Emulator memory, first → last sample | 15.42 → 22.57 GiB | 1.64 → 1.67 GiB |
| Whole stack, peak (emulator and application) | 38.83 GiB | 17.43 GiB |
| Emulator CPU time | 1,586 s | 616 s |
| Disk written by the whole server | 3.66 GB | 1.83 GB |
Most of what the official stack’s server wrote was swap: it paged out 3.1 GB, where Firenook’s paged out nothing. Firenook’s writes are its data, durable within a second. The application’s web server peaked at about 12.1 GiB on both.
Start, stop and start again
To keep its data, the official emulator exports at every stop and imports at every start. Firenook keeps its data and picks it up.
| Firestore, Auth and Storage, no application | Official | Firenook |
|---|---|---|
| First start, importing the data | 56.7 s | 28.3 s |
| Start again with the data | 30.7–31.7 s | 0.59–0.60 s |
| Stop with the data saved | 54–58 s (export) | 0.05–0.26 s |
| Memory once ready | 6.9 GiB | 0.16 GiB |
| Memory after the 26 query shapes | 11.9 GiB | 0.65 GiB |
No Firenook start needed a repair. With the whole application, its builds included, the stack was ready in 55.7 s against 86.5 s, again in 42.9 s against 86.8 s after a restart, and stopped in 29.6 s against 79.8 s.
A store grown to 2.7 million documents
The same data grown by 2,500,000 small documents, then started on each, with four queries on the grown collection. On Java’s defaults the official emulator ran out of memory at the first query.
| 2,711,202 documents | Official, Java defaults | Official, 24 GiB heap | Firenook |
|---|---|---|---|
| Start | 50.3 s, then out of memory | 55.2 s | 0.62 s |
| Memory | 14.5 → 16.2 GiB | 20.7 → 24.7 GiB | 0.11 GiB |
| orderBy a timestamp, descending, limit 20 | Failed | 8,550 ms | 4.3 ms |
| Range on an integer, limit 20 | Failed | 8,205 ms | 3.2 ms |
| == plus orderBy descending, limit 20 | Failed | 14,124 ms | 5.6 ms |
| == on a string, limit 20 | Failed | 27,562 ms | 3.4 ms |
The official emulator imported the data from a 9.81 GB export. Firenook walks an ordered index to the 20 documents asked for, however large the collection.
26 query shapes
Each shape warmed up once, then run 25 times over gRPC against the same data. Every answer was the same on both. Firenook was faster on 25 and tied on the last.
| Shape | Official p50 | Firenook p50 | Official p99 | Firenook p99 | Faster |
|---|---|---|---|---|---|
| Document id in 10 ids | 615 | 13.4 | 646 | 22.7 | Firenook, 46.0× |
| Collection, limit 1 | 88.6 | 2.60 | 90.4 | 3.32 | Firenook, 34.1× |
| Small collection: == on a string | 28.3 | 1.68 | 28.7 | 1.79 | Firenook, 16.8× |
| count() of a collection | 86.1 | 7.99 | 88.8 | 9.21 | Firenook, 10.8× |
| array-contains | 724 | 70.5 | 739 | 83.1 | Firenook, 10.3× |
| orderBy an integer field, limit 20 | 96.3 | 12.0 | 111 | 25.0 | Firenook, 8.0× |
| Range on an integer field, limit 20 | 95.5 | 13.2 | 118 | 22.2 | Firenook, 7.2× |
| == on a boolean, limit 20 | 108 | 15.3 | 118 | 23.9 | Firenook, 7.0× |
| == plus orderBy on another field | 99.3 | 14.7 | 113 | 23.8 | Firenook, 6.8× |
| Five cursor pages of 20 | 489 | 74.8 | 515 | 86.7 | Firenook, 6.5× |
| == on a string field | 124 | 20.0 | 139 | 29.7 | Firenook, 6.2× |
| array-contains-any of 5 values | 352 | 61.0 | 443 | 84.5 | Firenook, 5.8× |
| Second collection: array-contains plus orderBy a timestamp | 37.3 | 7.62 | 44.9 | 15.8 | Firenook, 4.9× |
| orderBy a timestamp, descending, limit 20 | 109 | 24.7 | 125 | 36.0 | Firenook, 4.4× |
| Collection group, limit 20 | 57.0 | 14.2 | 68.0 | 22.8 | Firenook, 4.0× |
| count() with == on a boolean | 83.8 | 24.6 | 104 | 29.8 | Firenook, 3.4× |
| offset 1000, limit 20 | 102 | 32.0 | 109 | 45.7 | Firenook, 3.2× |
| Collection, limit 100 | 210 | 78.3 | 231 | 92.4 | Firenook, 2.7× |
| Second collection: == on a boolean, limit 50 | 116 | 45.2 | 131 | 63.7 | Firenook, 2.6× |
| Second collection: == plus != | 61.4 | 26.5 | 73.7 | 38.2 | Firenook, 2.3× |
| Listener: first snapshot of 20 documents | 115 | 51.0 | 133 | 62.1 | Firenook, 2.2× |
| Batch get of 50 documents | 80.6 | 43.8 | 165 | 58.4 | Firenook, 1.8× |
| Get one document by id | 5.26 | 3.31 | 20.6 | 4.74 | Firenook, 1.6× |
| Write one document, then delete it | 3.01 | 1.96 | 4.16 | 3.56 | Firenook, 1.5× |
| Transaction: read, modify, write | 5.51 | 3.95 | 5.97 | 12.5 | Firenook, 1.4× |
| Ids only, limit 500 | 97.8 | 98.3 | 142 | 115 | Tie |
Milliseconds; Firenook’s figure is the median of three rounds. Firenook serves orderBy, range filters and cursors on a top-level field from an ordered index, and checks equality filters on other fields before it reads a document. The tie, 500 document ids with no fields, reads each document whole before dropping its fields; reading only the ids is a next step.
Writes on their own
Firestore alone on the same server, on fresh copies of the same store: 2,000 writes one at a time, each confirmed by a listener, then 4,000 from eight writers at once.
| Round | Write, median / p99 | Listener, median | Eight writers |
|---|---|---|---|
| 1 | 0.67 / 1.17 ms | 0.73 ms | 3,078 writes/s |
| 2 | 0.62 / 1.12 ms | 0.68 ms | 3,374 writes/s |
On its own a write commits in under a millisecond; inside the application, with its functions, listeners and web server on the same machine, the median was 5.1 ms.
How it was kept fair
- One emulator at a time on an otherwise idle server, from a clean start, swap emptied before each stack started.
- The official emulator ran with Java’s default settings, as a developer gets it, except where a row says otherwise. Firenook ran its defaults: data on disk, acknowledged writes durable within a second.
- Same data, same application revision, same workload; the run fails unless both stacks end with identical data.
- Both stacks started the application with an empty build cache.
- Every percentile is computed from all recorded operations, and an independent audit recomputed every figure from the raw samples and verified the 95 checksummed evidence files.
- The application’s own scheduled jobs were kept outside both measuring windows.
Its limits
- One server and one application. Measure your own workload before relying on these numbers.
- The official stack used more memory than the server could hold: it already had 7.7 GB in swap when its two hours began and paged out 3.1 GB more, against 1.6 GB and nothing more on Firenook. On a machine with more memory the official emulator would swap less.
- The application’s start times include its own builds; the emulator-only starts and stops are the ones that isolate the emulators.
- One browser journey of the application waits on its cache worker, which the official emulator wakes early by notifying a listener for a document the listener’s filter excludes; Firenook does not, so that journey runs longer on Firenook. It is not an emulator latency and is left out.
The full write-up is in the engine’s docs, and every sample in the record.
Measure your own workload
The application and its data are private, so this run can’t be repeated as it is. Your own tests are the measure that matters: run them on both, on the same machine.
$ time npx firebase emulators:exec "npm test"
$ time npx firenook emulators:exec "npm test"
npm install --save-dev firenook