On this page
Why
Since firebase-tools 15, the official Cloud Firestore emulator requires Java 21 or newer. You install a JDK to run a local database, and your CI runs setup-java and downloads a jar for the same reason. It had bothered me for a long time.
Measuring turned the bother into numbers. In my benchmark environment (AWS c7i.xlarge, Ubuntu 24.04, Java 21, firebase-tools 15.33.0), the official emulator v1.22.0 takes 981 ms until its port accepts a connection and sits at 148 MiB idle. With one listener attached, writing in commits of 500 gets slower as data accumulates: each commit takes 195 ms longer per 10,000 documents already stored, and 100,000 documents take 191 seconds. The regression has been known for years as firebase-tools#3477.
The official emulator is a closed-source Java application, so it cannot be fixed from outside. It had to be replaced.
What I built
hidane (火種, “the seed of fire”) is a single Rust binary that works as a drop-in replacement.

- It speaks gRPC, REST and WebChannel on one port, as the official emulator does
- It evaluates Security Rules
- Exports and imports use the official format, and each emulator reads the other’s files
- It runs under
firebase-tools: puthidane exec --in front offirebase emulators:start
curl -fsSL https://hidane.dev/install.sh | sh # or brew / npm / cargo / dart pub / Docker
hidane exec -- firebase emulators:exec "npm test"
On the same machine, hidane v0.6.0 opens its port in 1.56 ms, idles at 9.4 MiB, and writes the 100,000 documents with a listener attached in 2.51 seconds. A commit costs only what it changes, independent of how many documents are stored.
One honest caveat: the Admin SDKs send an emulator the owner credential, which skips Security Rules, and under that condition the official emulator writes almost as fast as hidane. The gap is large for signed-out clients and with a listener attached. The fairness rules are written down in bench/.
Decisions
The design decisions live as ADRs in docs/adr/. Three of the big ones:
In-memory storage (ADR 0002). The official emulator does not persist either; export and import play that role. hidane does the same. Each commit gets a monotonically increasing sequence number, which is the source of read_time, and recent commits are kept as a change log so a listener can resume from a resume_token. Change notification costs O(changed documents) per commit. That is where the listener benchmark comes from.
Launching under firebase-tools (ADR 0006). firebase-tools probes java -version and then runs java -jar <jar>. There is no supported way to start anything but the jar. So hidane exec -- <command> puts a temporary directory first on PATH with something named java in it, which is hidane itself. It answers the version probe with 21 and starts itself with the arguments after -jar. It applies to that one command and leaves the machine’s java alone.
Proving compatibility (ADR 0008). The official jar is a black box that may be neither redistributed nor decompiled. Scripts in tools/oracle/ ask it questions over the wire and record the answers as JSON fixtures. CI replays every fixture against hidane without Java. In v0.6.0 that is 57 recordings and 26,556 recorded answers. Differences that are intentional, such as the official emulator’s performance bugs and known defects, are listed in docs/parity-exceptions.md, each naming the tests that pin it.
The whole thing is a clean-room implementation: the public protobuf definitions, the public documentation and black-box observation of the official emulator. docs/clean-room/ records where each behaviour may come from.
Around the emulator
Since v0.5 the tooling has grown. hidane doctor checks a project before it moves, without changing anything; hidane migrate shows the diff and applies it with --apply. hidane benchmark runs your own tests against both emulators, and savings turns the medians into CI minutes and cost per month. The setup-hidane GitHub Action verifies the release’s SHA-256 and build provenance before installing. In Testcontainers, hidane’s image stands in for the Cloud SDK image.
The benchmark lives in benchmark-results: both emulators measured on the same machine every night and for every release, raw samples included. Every number the README quotes links to the result it comes from.
What I learned
- If you claim speed, publish the method. Medians, not a lucky run, in a form anyone can rerun with the same script
- Claim compatibility only where a test exists. Showing verified and not verified side by side lets people decide for themselves
- A differential fuzzer, sending the same request to both emulators and comparing, found differences no one would have thought to test. v0.6.0 alone fixed six batches of them

Next
Flutter on mobile and desktop speaks the same gRPC transport but nobody has compared it yet. The enterprise edition’s pipelines (ExecutePipeline) are not implemented. Both are tracked in Issues. If you find a parity gap, please attach the output of hidane report.
RELATED
Flutter, AWS, homelab. A few side projects and open-source tools. For architecture questions or collaboration, reach out via X DM or the About page. About