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 percentileOfficialFirenookMedian
Write, acknowledged208.2 / 279.8 ms5.1 / 50.6 ms40× faster
Listener update after a write209 / 258 ms11 / 60 ms19× faster
Filtered query under load33.1 / 44.1 ms6.6 / 31.0 ms5.0× faster
The same query from an idle client32.6 / 43.8 ms7.1 / 30.9 ms4.5× faster
Firestore-triggered function3.80 / 8.02 ms3.11 / 4.49 ms1.2× faster
Storage upload, metadata, download, delete17.8 / 26.8 ms8.6 / 11.8 ms2.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 hoursOfficialFirenook
Emulator memory, peak (PSS)22.57 GiB (Java)2.11 GiB
Emulator memory, first → last sample15.42 → 22.57 GiB1.64 → 1.67 GiB
Whole stack, peak (emulator and application)38.83 GiB17.43 GiB
Emulator CPU time1,586 s616 s
Disk written by the whole server3.66 GB1.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 applicationOfficialFirenook
First start, importing the data56.7 s28.3 s
Start again with the data30.7–31.7 s0.59–0.60 s
Stop with the data saved54–58 s (export)0.05–0.26 s
Memory once ready6.9 GiB0.16 GiB
Memory after the 26 query shapes11.9 GiB0.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 documentsOfficial, Java defaultsOfficial, 24 GiB heapFirenook
Start50.3 s, then out of memory55.2 s0.62 s
Memory14.5 → 16.2 GiB20.7 → 24.7 GiB0.11 GiB
orderBy a timestamp, descending, limit 20Failed8,550 ms4.3 ms
Range on an integer, limit 20Failed8,205 ms3.2 ms
== plus orderBy descending, limit 20Failed14,124 ms5.6 ms
== on a string, limit 20Failed27,562 ms3.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.

ShapeOfficial p50Firenook p50Official p99Firenook p99Faster
Document id in 10 ids61513.464622.7Firenook, 46.0×
Collection, limit 188.62.6090.43.32Firenook, 34.1×
Small collection: == on a string28.31.6828.71.79Firenook, 16.8×
count() of a collection86.17.9988.89.21Firenook, 10.8×
array-contains72470.573983.1Firenook, 10.3×
orderBy an integer field, limit 2096.312.011125.0Firenook, 8.0×
Range on an integer field, limit 2095.513.211822.2Firenook, 7.2×
== on a boolean, limit 2010815.311823.9Firenook, 7.0×
== plus orderBy on another field99.314.711323.8Firenook, 6.8×
Five cursor pages of 2048974.851586.7Firenook, 6.5×
== on a string field12420.013929.7Firenook, 6.2×
array-contains-any of 5 values35261.044384.5Firenook, 5.8×
Second collection: array-contains plus orderBy a timestamp37.37.6244.915.8Firenook, 4.9×
orderBy a timestamp, descending, limit 2010924.712536.0Firenook, 4.4×
Collection group, limit 2057.014.268.022.8Firenook, 4.0×
count() with == on a boolean83.824.610429.8Firenook, 3.4×
offset 1000, limit 2010232.010945.7Firenook, 3.2×
Collection, limit 10021078.323192.4Firenook, 2.7×
Second collection: == on a boolean, limit 5011645.213163.7Firenook, 2.6×
Second collection: == plus !=61.426.573.738.2Firenook, 2.3×
Listener: first snapshot of 20 documents11551.013362.1Firenook, 2.2×
Batch get of 50 documents80.643.816558.4Firenook, 1.8×
Get one document by id5.263.3120.64.74Firenook, 1.6×
Write one document, then delete it3.011.964.163.56Firenook, 1.5×
Transaction: read, modify, write5.513.955.9712.5Firenook, 1.4×
Ids only, limit 50097.898.3142115Tie

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.

RoundWrite, median / p99Listener, medianEight writers
10.67 / 1.17 ms0.73 ms3,078 writes/s
20.62 / 1.12 ms0.68 ms3,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.

Terminal
            
            
                $ time npx firebase emulators:exec "npm test"
              
          
            

            
                $ time npx firenook emulators:exec "npm test"
              
          
npm install --save-dev firenook
Run your tests on Firenook