Skip to content

Benchmarks

How fast is yay-machine compared with XState, and does it matter for your app?

We used to publish a single “toggle” benchmark. It’s the most flattering test we have, and it doesn’t tell you much about real apps. This page publishes every benchmark we run, including the realistic ones where the gap is smaller, along with how they’re measured.

Each bar shows how many times faster (or, for memory, how many times leaner) yay-machine is than XState for the same work. 1× would mean the two are even.

yay-machine vs XState: all benchmarks UI: sign-up form, form session (38 events): yay-machine 1.30M events/s, XState 162k events/s; 8.1× faster. Event stream: long-lived connection, replay 1,000-event trace: yay-machine 5.24M events/s, XState 283k events/s; 19× faster. Fleet: 1,000 live connections, 1 event to each of 1,000 instances: yay-machine 2.18M events/s, XState 192k events/s; 11× faster. Login request, login success + logout: yay-machine 271k requests/s, XState 18.9k requests/s; 14× faster. Login request, login rejected + retry: yay-machine 320k requests/s, XState 18.0k requests/s; 18× faster. Memory per live instance, started, idle (disconnected): yay-machine 1.23 KiB per instance, XState 2.69 KiB per instance; 2.2× less memory. Memory per live instance, connected (with heartbeat resource): yay-machine 1.33 KiB per instance, XState 3.94 KiB per instance; 3.0× less memory. Toggle, send TOGGLE: yay-machine 3.53M events/s, XState 164k events/s; 22× faster. Counter, INCREMENT + DECREMENT (guards pass): yay-machine 1.35M events/s, XState 254k events/s; 5.3× faster. Counter, INCREMENT beyond max (guard rejects): yay-machine 9.29M events/s, XState 828k events/s; 11× faster. Guards, 1st guard matches: yay-machine 2.08M events/s, XState 268k events/s; 7.8× faster. Guards, 3rd guard matches: yay-machine 1.79M events/s, XState 287k events/s; 6.3× faster. Guards, default (5th) matches: yay-machine 1.93M events/s, XState 236k events/s; 8.2× faster. Immediate transitions, routes via 1st branch: yay-machine 1.61M events/s, XState 91.0k events/s; 18× faster. Immediate transitions, routes via last branch: yay-machine 1.27M events/s, XState 89.3k events/s; 14× faster. Event resolution, handled by current state: yay-machine 2.00M events/s, XState 249k events/s; 8.0× faster. Event resolution, falls through to machine-level: yay-machine 2.05M events/s, XState 221k events/s; 9.3× faster. Event resolution, not handled (ignored): yay-machine 28.5M events/s, XState 670k events/s; 43× faster. State effects, entry + exit actions: yay-machine 2.01M events/s, XState 129k events/s; 16× faster. State effects, state-scoped resource + cleanup: yay-machine 1.96M events/s, XState 63.9k events/s; 31× faster. Lifecycle, define a 5-state machine: yay-machine 593k definitions/s, XState 37.4k definitions/s; 16× faster. Lifecycle, create + start + stop: yay-machine 1.18M lifecycles/s, XState 92.7k lifecycles/s; 13× faster. Lifecycle, ... with machine-scoped resource: yay-machine 1.09M lifecycles/s, XState 31.1k lifecycles/s; 35× faster. Batch of short-lived instances, batch of 10: yay-machine 870k lifecycles/s, XState 50.3k lifecycles/s; 17× faster. yay-machine vs XState: all benchmarks How many times faster (or leaner) yay-machine is than XState for each benchmark. Log scale · 1× = parity · bars left of 1× would mean XState is ahead 0.5×1×2×5×10×20×50×REALISTIC WORKLOADSUI: sign-up formform session (38 events)8.1×Event stream: long-lived connectionreplay 1,000-event trace19×Fleet: 1,000 live connections1 event to each of 1,000 instances11×Login requestlogin success + logout14×Login requestlogin rejected + retry18×Memory per live instancestarted, idle (disconnected)2.2× less memoryMemory per live instanceconnected (with heartbeat resource)3.0× less memoryFEATURE MICRO-BENCHMARKSTogglesend TOGGLE22×CounterINCREMENT + DECREMENT (guards pass)5.3×CounterINCREMENT beyond max (guard rejects)11×Guards1st guard matches7.8×Guards3rd guard matches6.3×Guardsdefault (5th) matches8.2×Immediate transitionsroutes via 1st branch18×Immediate transitionsroutes via last branch14×Event resolutionhandled by current state8.0×Event resolutionfalls through to machine-level9.3×Event resolutionnot handled (ignored)43×State effectsentry + exit actions16×State effectsstate-scoped resource + cleanup31×Lifecycledefine a 5-state machine16×Lifecyclecreate + start + stop13×Lifecycle... with machine-scoped resource35×Batch of short-lived instancesbatch of 1017× yay-machine v1.5.0+297 (4179cf1) · xstate 5.33.3 · Bun 1.4.3 · 2026-11-09 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz, 8 cores, 8 GB · Linux (WSL2) (x64)

What we take from this:

  • yay-machine was faster in every benchmark, and used less memory per instance.
  • The gap shrinks as more time is spent in your code. Building state-data, evaluating guards and running subscribers costs the same with either library, so benchmarks that do more of it (like the sign-up form, or counter updates) show smaller gaps than the bare toggle.
  • The gap is biggest where yay-machine simply does less: ignored events, starting and stopping machines, and acquiring and releasing resources.
  • Will you notice? In a typical UI handling a few events per second, probably not: both libraries handle each event in microseconds or less. It starts to matter when you send lots of events (streams, parsers, games), run lots of machines (one per connection, per request, per entity), or care about memory per instance.

These model the kinds of machines people actually build, with state-data, guards, side-effects and subscribers, driven by realistic sequences of events.

A subscribed sign-up form, driven through a typical session of 38 events: typing into fields (with a state-data update per keystroke), validation via an immediate transition, a submit that fails, a submit that succeeds (with abort cleanup of the in-flight request), and reset. A subscriber derives a view-model from every state, like a UI component would.

Workload: UI: sign-up form A subscribed UI form session: per-keystroke data updates, validation via an immediate transition, a failed then successful submit request (with abort cleanup), and reset. UI: sign-up form, form session (38 events): yay-machine 1.30M events/s, XState 162k events/s; 8.1× faster. Workload: UI: sign-up form A subscribed UI form session: per-keystroke data updates, validation via an immediate transition, a failed then successful submit request (with abort cleanup), and reset. yay-machineXState form session (38 events)1 op = 38 events1.30M events/s ±5.4%162k events/s ±6.5%8.1× faster yay-machine v1.5.0+297 (4179cf1) · xstate 5.33.3 · Bun 1.4.3 · 2026-11-09 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz, 8 cores, 8 GB · Linux (WSL2) (x64)

One long-lived connection machine replaying a seeded, realistic trace of 1,000 events: mostly sends and heartbeats, with occasional errors and reconnects (each acquiring and releasing a connection resource), and some stray events that the current state ignores.

Workload: Event stream: long-lived connection One long-lived connection machine replaying a realistic, mixed 1,000-event trace: sends, heartbeats, errors, reconnects (with resource cleanup) and stray events that are ignored. Event stream: long-lived connection, replay 1,000-event trace: yay-machine 5.24M events/s, XState 283k events/s; 19× faster. Workload: Event stream: long-lived connection One long-lived connection machine replaying a realistic, mixed 1,000-event trace: sends, heartbeats, errors, reconnects (with resource cleanup) and stray events that are ignored. yay-machineXState replay 1,000-event trace1 op = 1000 events5.24M events/s ±5.7%283k events/s ±9.5%19× faster yay-machine v1.5.0+297 (4179cf1) · xstate 5.33.3 · Bun 1.4.3 · 2026-11-09 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz, 8 cores, 8 GB · Linux (WSL2) (x64)

The same connection machine, but 1,000 instances in assorted states, as a server holding one machine per client would. Each operation delivers the next event from the trace to every instance, which tests how each library behaves when its working set doesn’t fit in the CPU’s caches.

Workload: Fleet: 1,000 live connections 1,000 long-lived connection machines in assorted states; each op delivers one event from the realistic trace to every instance. Fleet: 1,000 live connections, 1 event to each of 1,000 instances: yay-machine 2.18M events/s, XState 192k events/s; 11× faster. Workload: Fleet: 1,000 live connections 1,000 long-lived connection machines in assorted states; each op delivers one event from the realistic trace to every instance. yay-machineXState 1 event to each of 1,000instances1 op = 1000 events2.18M events/s ±8.6%192k events/s ±9.0%11× faster yay-machine v1.5.0+297 (4179cf1) · xstate 5.33.3 · Bun 1.4.3 · 2026-11-09 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz, 8 cores, 8 GB · Linux (WSL2) (x64)

A short-lived machine per request: create and start, LOGIN through a guarded immediate transition, then LOGOUT (or a rejected login and RETRY), and stop. Includes entry/exit side-effects and a session resource that must be cleaned up.

Workload: Login request A short-lived, per-request login workflow: create + start, LOGIN through a guarded immediate transition, LOGOUT/RETRY, stop. Includes entry/exit effects and a session resource. Login request, login success + logout: yay-machine 271k requests/s, XState 18.9k requests/s; 14× faster. Login request, login rejected + retry: yay-machine 320k requests/s, XState 18.0k requests/s; 18× faster. Workload: Login request A short-lived, per-request login workflow: create + start, LOGIN through a guarded immediate transition, LOGOUT/RETRY, stop. Includes entry/exit effects and a session resource. yay-machineXState login success + logout1 op = 1 request (create, 2events, stop)271k requests/s ±9.0%18.9k requests/s ±10.2%14× fasterlogin rejected + retry1 op = 1 request (create, 2events, stop)320k requests/s ±4.9%18.0k requests/s ±10.6%18× faster yay-machine v1.5.0+297 (4179cf1) · xstate 5.33.3 · Bun 1.4.3 · 2026-11-09 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz, 8 cores, 8 GB · Linux (WSL2) (x64)

Retained heap per running connection machine, measured across 10,000 instances, idle and connected (holding a heartbeat resource). This matters when a server keeps one machine per client.

Workload: Memory per live instance Retained heap per running connection machine, measured across 10,000 instances. Matters when a server holds one machine per client. Memory per live instance, started, idle (disconnected): yay-machine 1.23 KiB per instance, XState 2.69 KiB per instance; 2.2× less memory. Memory per live instance, connected (with heartbeat resource): yay-machine 1.33 KiB per instance, XState 3.94 KiB per instance; 3.0× less memory. Workload: Memory per live instance Retained heap per running connection machine, measured across 10,000 instances. Matters when a server holds one machine per client. yay-machineXState started, idle(disconnected)bytes/instance, lower is better1.23 KiB ±0.0%2.69 KiB ±0.0%2.2× less memoryconnected (with heartbeatresource)bytes/instance, lower is better1.33 KiB ±0.0%3.94 KiB ±0.0%3.0× less memory yay-machine v1.5.0+297 (4179cf1) · xstate 5.33.3 · Bun 1.4.3 · 2026-11-09 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz, 8 cores, 8 GB · Linux (WSL2) (x64)

These isolate individual features, so you can see where the time goes. They’re useful for spotting regressions, but on their own they say little about real apps.

Two states, one event, no state-data or side-effects. Pure per-event overhead, and the benchmark we used to publish on its own.

Micro-benchmark: Toggle Two states, one event, no data or effects. Pure per-event dispatch overhead. Toggle, send TOGGLE: yay-machine 3.53M events/s, XState 164k events/s; 22× faster. Micro-benchmark: Toggle Two states, one event, no data or effects. Pure per-event dispatch overhead. yay-machineXState send TOGGLE1 op = 1 event3.53M events/s ±8.8%164k events/s ±8.9%22× faster yay-machine v1.5.0+297 (4179cf1) · xstate 5.33.3 · Bun 1.4.3 · 2026-11-09 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz, 8 cores, 8 GB · Linux (WSL2) (x64)

State-data updated from event payloads, with the guard accepting or rejecting the update.

Micro-benchmark: Counter Guarded context/data updates from event payloads, with and without the guard rejecting the update. Counter, INCREMENT + DECREMENT (guards pass): yay-machine 1.35M events/s, XState 254k events/s; 5.3× faster. Counter, INCREMENT beyond max (guard rejects): yay-machine 9.29M events/s, XState 828k events/s; 11× faster. Micro-benchmark: Counter Guarded context/data updates from event payloads, with and without the guard rejecting the update. yay-machineXState INCREMENT + DECREMENT(guards pass)1 op = 2 events1.35M events/s ±9.5%254k events/s ±10.1%5.3× fasterINCREMENT beyond max(guard rejects)1 op = 1 event9.29M events/s ±11.8%828k events/s ±10.0%11× faster yay-machine v1.5.0+297 (4179cf1) · xstate 5.33.3 · Bun 1.4.3 · 2026-11-09 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz, 8 cores, 8 GB · Linux (WSL2) (x64)

One event with five ordered, guarded candidate transitions, matching on the first, third or last (default) candidate.

Micro-benchmark: Guards One event with five ordered, guarded candidate transitions. Measures guard evaluation as the match moves down the list. Guards, 1st guard matches: yay-machine 2.08M events/s, XState 268k events/s; 7.8× faster. Guards, 3rd guard matches: yay-machine 1.79M events/s, XState 287k events/s; 6.3× faster. Guards, default (5th) matches: yay-machine 1.93M events/s, XState 236k events/s; 8.2× faster. Micro-benchmark: Guards One event with five ordered, guarded candidate transitions. Measures guard evaluation as the match moves down the list. yay-machineXState 1st guard matches1 op = 1 event2.08M events/s ±11.2%268k events/s ±9.1%7.8× faster3rd guard matches1 op = 1 event1.79M events/s ±6.6%287k events/s ±6.0%6.3× fasterdefault (5th) matches1 op = 1 event1.93M events/s ±7.7%236k events/s ±8.0%8.2× faster yay-machine v1.5.0+297 (4179cf1) · xstate 5.33.3 · Bun 1.4.3 · 2026-11-09 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz, 8 cores, 8 GB · Linux (WSL2) (x64)

An event enters a transient decision state, which immediately routes onwards via guarded immediate transitions (always in both libraries).

Micro-benchmark: Immediate transitions An event enters a transient decision state which immediately routes onwards via guarded 'always' transitions, then RESET. Immediate transitions, routes via 1st branch: yay-machine 1.61M events/s, XState 91.0k events/s; 18× faster. Immediate transitions, routes via last branch: yay-machine 1.27M events/s, XState 89.3k events/s; 14× faster. Micro-benchmark: Immediate transitions An event enters a transient decision state which immediately routes onwards via guarded 'always' transitions, then RESET. yay-machineXState routes via 1st branch1 op = 2 events1.61M events/s ±6.2%91.0k events/s ±9.6%18× fasterroutes via last branch1 op = 2 events1.27M events/s ±8.9%89.3k events/s ±9.2%14× faster yay-machine v1.5.0+297 (4179cf1) · xstate 5.33.3 · Bun 1.4.3 · 2026-11-09 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz, 8 cores, 8 GB · Linux (WSL2) (x64)

An event handled by the current state, falling through to a machine-level handler, or not handled at all.

Micro-benchmark: Event resolution An event handled by the current state, falling through to a machine-level handler, or not handled at all. Event resolution, handled by current state: yay-machine 2.00M events/s, XState 249k events/s; 8.0× faster. Event resolution, falls through to machine-level: yay-machine 2.05M events/s, XState 221k events/s; 9.3× faster. Event resolution, not handled (ignored): yay-machine 28.5M events/s, XState 670k events/s; 43× faster. Micro-benchmark: Event resolution An event handled by the current state, falling through to a machine-level handler, or not handled at all. yay-machineXState handled by current state1 op = 1 event2.00M events/s ±9.7%249k events/s ±9.4%8.0× fasterfalls through tomachine-level1 op = 1 event2.05M events/s ±6.2%221k events/s ±4.8%9.3× fasternot handled (ignored)1 op = 1 event28.5M events/s ±4.2%670k events/s ±10.4%43× faster yay-machine v1.5.0+297 (4179cf1) · xstate 5.33.3 · Bun 1.4.3 · 2026-11-09 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz, 8 cores, 8 GB · Linux (WSL2) (x64)

Entering and leaving a state that has entry/exit side-effects, or that acquires and releases a resource for as long as it’s active.

Micro-benchmark: State effects ACTIVATE then DEACTIVATE with data updates, running per-state entry/exit side-effects, or acquiring and releasing a state-scoped resource. State effects, entry + exit actions: yay-machine 2.01M events/s, XState 129k events/s; 16× faster. State effects, state-scoped resource + cleanup: yay-machine 1.96M events/s, XState 63.9k events/s; 31× faster. Micro-benchmark: State effects ACTIVATE then DEACTIVATE with data updates, running per-state entry/exit side-effects, or acquiring and releasing a state-scoped resource. yay-machineXState entry + exit actions1 op = 2 events2.01M events/s ±8.3%129k events/s ±7.2%16× fasterstate-scoped resource +cleanup1 op = 2 events1.96M events/s ±9.2%63.9k events/s ±4.4%31× faster yay-machine v1.5.0+297 (4179cf1) · xstate 5.33.3 · Bun 1.4.3 · 2026-11-09 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz, 8 cores, 8 GB · Linux (WSL2) (x64)

Defining a machine, and creating, starting and stopping an instance, with and without a machine-scoped resource.

Micro-benchmark: Lifecycle Defining a 5-state machine, and create + start + stop of an instance with and without a machine-scoped resource. Lifecycle, define a 5-state machine: yay-machine 593k definitions/s, XState 37.4k definitions/s; 16× faster. Lifecycle, create + start + stop: yay-machine 1.18M lifecycles/s, XState 92.7k lifecycles/s; 13× faster. Lifecycle, ... with machine-scoped resource: yay-machine 1.09M lifecycles/s, XState 31.1k lifecycles/s; 35× faster. Micro-benchmark: Lifecycle Defining a 5-state machine, and create + start + stop of an instance with and without a machine-scoped resource. yay-machineXState define a 5-state machine1 op = 1 definition593k definitions/s ±2.7%37.4k definitions/s ±4.9%16× fastercreate + start + stop1 op = 1 lifecycle1.18M lifecycles/s ±2.2%92.7k lifecycles/s ±3.6%13× faster... with machine-scopedresource1 op = 1 lifecycle1.09M lifecycles/s ±7.4%31.1k lifecycles/s ±10.6%35× faster yay-machine v1.5.0+297 (4179cf1) · xstate 5.33.3 · Bun 1.4.3 · 2026-11-09 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz, 8 cores, 8 GB · Linux (WSL2) (x64)

Create and start 10 instances, send each one an event, and stop them all.

Micro-benchmark: Batch of short-lived instances Create + start 10 instances, send each one event, then stop them all. Batch of short-lived instances, batch of 10: yay-machine 870k lifecycles/s, XState 50.3k lifecycles/s; 17× faster. Micro-benchmark: Batch of short-lived instances Create + start 10 instances, send each one event, then stop them all. yay-machineXState batch of 101 op = 10 lifecycles870k lifecycles/s ±1.5%50.3k lifecycles/s ±7.0%17× faster yay-machine v1.5.0+297 (4179cf1) · xstate 5.33.3 · Bun 1.4.3 · 2026-11-09 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz, 8 cores, 8 GB · Linux (WSL2) (x64)

The benchmark source is on GitHub. We tried hard to make the comparisons fair:

  • Equivalent work. Each benchmark has a hand-written XState and yay-machine implementation, written idiomatically for each library. A test checks that both reach the same states and state-data, and run the same side-effects and cleanups the same number of times, before and after each benchmark iteration.
  • Production builds. Both libraries run from their published, production builds.
  • Isolation. Each suite runs in a separate process, so JIT optimisations from one suite can’t influence another, and the order of the two libraries alternates between variants.
  • Throughput is measured with Benny (Benchmark.js), sampling each case for several seconds. Each chart shows the mean rate, and its relative margin of error at 95% confidence (±%).
  • Memory is measured in a fresh process per sample: warm up, garbage collect, create 10,000 instances, garbage collect, and divide the growth in heap size by the number of instances. The chart shows the median of 5 samples.
  • A ratio can move by a third or more between runs, much more than the ±% error bars suggest: those only capture variation within a run, not things like CPU frequency scaling between runs. Look at the overall picture, not the exact numbers.
  • These results come from one machine, running Bun (JavaScriptCore). Results on Node.js or in the browser (V8) will differ, and so may the ratios.
  • The libraries don’t behave identically, and we didn’t try to hide that. For example, XState notifies subscribers about every event, even when nothing changes, while yay-machine only notifies them when the machine transitions. That’s a real cost of using XState, so it’s included.
  • XState does more than yay-machine: actors, hierarchical and parallel states, delayed events, inspection, and more. You pay for some of that even when you don’t use it, but if you need those features, the comparison isn’t like-for-like.

We don’t re-run the benchmarks on every docs build: they take a while, and need a quiet machine. Instead we re-run them when something significant changes, such as a new release of either library.

Terminal window
bun run bench:report # run every suite, save the results, then render the charts
bun run bench:charts # just re-render the charts from the saved results

To try a single suite, or get a faster, rougher read while developing, run these in packages/bench:

Terminal window
bun run bench:toggle # a single suite
bun run bench:quick # all suites, fewer samples
bun run src/bench.ts form stream --quick # pick suites

Quick and partial runs aren’t saved, unless you add --save.