Skip to content

Competitive Proof — ReactiveGraph vs LangGraph / LangChain

This report only claims capabilities that are backed by reproducible commands, pinned upstream packages, and committed tests. It does not claim ecosystem parity.

1. Same-output performance matrix

The benchmark orchestrator first asserts output equality; timing is printed only after outputs match. This prevents comparing two engines that silently produced different results.

1a. Performance and selective execution

Scope of the millisecond figures below. The differential harness measures the native Driver Scheduler directly (harness.test.ts constructs a Scheduler and calls runAll). That is a component-level comparison against upstream's graph.invoke(), and it does not include the RGP/1 hop or the Node child-process boundary that a Python host actually pays. Read the ratios as "engine-core vs engine-core", not as end-to-end user latency. Section 1b reports the end-to-end path separately, because the two differ by orders of magnitude.

The differential benchmark runs the same deterministic graphs under ReactiveGraph and upstream langgraph==1.2.11, asserts identical outputs, and reports medians.

Run:

uv sync --directory benchmarks/differential
uv run --directory benchmarks/differential python run_differential.py --refresh

CI publishes the raw differential result JSON as the differential-benchmark-matrix artifact from the manual benchmark workflow.

Latest committed evidence is in:

  • benchmarks/differential/.results/reactive.json
  • benchmarks/differential/.results/langgraph.json
  • benchmarks/deerflow/SELECTIVE_GAIN_REPORT.md

Key architectural result:

Workload Reactive behavior LangGraph behavior
Wide fan-out, one changed input executes only affected tasks full reachable graph walk
Selective, 10 affected nodes executes affected subset full reachable graph walk
Same input rerun pure tasks skip nodes rerun
No-gain fully changed chain all tasks run all nodes run

The no-gain row is always reported. Selective execution is not claimed where nothing can be skipped.

1b. End-to-end vs engine-core: a measured correction

The component-level numbers above were, at one point, the only numbers in this document, and they understated the real Python cost by ~1500x. A 1000-node run that executes only 10 tasks measured 2.5 ms through the bare Scheduler but 3969 ms through the real Python path (RGP/1 + Node child process), before the fix described below.

Root cause (found by driving the real path, not the component): DriverRuntime rebuilt the merged task input — {...store.raw, ...baseInput} — once per task inside the dispatch loop. With a 1000-node graph the thread store holds ~2000 keys, and a single V8 spread of a 2000-key dictionary object measured ~4.6 ms, so a run that executed zero tasks still burned ~3.1 s of pure CPU. The executor invocation count was the proof the cost was the copy and not the work: 0 callbacks were invoked.

Fix: the merge is now lazy (Scheduler.runTaskLazy), invoked only for tasks that actually execute. The per-task copy isolation callers rely on is preserved — a first fix that memoised one shared object was rejected because it broke input-pollution isolation between tasks.

Path (1000 nodes, 10 affected, median of 3) Before After
Bare Scheduler.runAll 16.4 ms 16.4 ms
DriverRuntime.run (full TS engine) 3104 ms 255 ms
Python ReactiveGraph.invoke over RGP/1 + Node 3969 ms 257 ms

With the real path measured honestly, the end-to-end comparison against upstream is a single-digit multiple, not four digits.

Only one row is a valid comparison. The differential harness gives LangGraph a fresh thread_id per run, so its cache can never hit, while the reactive side reuses one scheduler across runs. Comparing those two numbers would credit us for a difference in threading, not in engines. The fair row gives LangGraph the same same-thread reuse and a hand-written per-node key_func (its best possible configuration):

End-to-end (1000 nodes, 10 affected, same-thread reuse) ReactiveGraph LangGraph Ratio
Python invoke vs upstream with per-node key_func 257 ms 2278 ms ~9x

For reference, upstream's default CachePolicy() never hits in this shape (1000/1000 nodes execute), and the un-fair fresh-thread figure is ~5942 ms — quoted here only so the two numbers above are not confused.

Reproduce the end-to-end row by driving DriverHost (see python/reactivegraph/tests/test_driver_scheduling.py for the harness shape); the regression guard is packages/driver/test/scheduler/task-input-scaling.test.ts, which fails if a fully-skipped 1000-node run takes longer than one second.

2. No-gain matrix

The no-gain workload is intentionally included:

Workload Expectation Meaning
Fully changed dependency chain all tasks execute reactive selection has nothing to skip
Changed query in RAG retrieval/model tasks rerun only unchanged segments skip
Distinct tool inputs tools execute receipt is input-scoped

This matrix prevents cherry-picking only favorable selective workloads.

2a. Duplicate side-effect matrix

ReactiveGraph records an effect intent before an external call and a receipt only after its patch commits. On restart, a matching input plus confirmed receipt skips the effect.

Evidence:

uv run --directory python/reactivechain \
  python -m pytest tests/test_tools.py -q
uv run --directory python/reactivechain \
  python -m pytest benchmarks/deerflow/ -q

The tool suite covers same-run and Driver-restart receipt reuse.

3. Explainability comparison

ReactiveGraph exposes native decision queries:

g.explain_run()
g.explain_run(run_id="...")
g.why_skipped("task")
g.why_invalidated("selector")
g.explain_conflict()
g.export_trace(format="json")
g.export_trace(format="dot")
g.cost_saved()

The comparison point is semantic, not cosmetic: LangGraph streams updates and checkpoints; ReactiveGraph records the runtime decision itself (skip reason, invalidation read-set, conflict winner/loser/write sets, estimated savings).

Evidence:

uv run --directory python/reactivegraph \
  python -m pytest tests/test_observability.py -q

4. Correctness model

ReactiveGraph tasks have a runtime contract:

  • kind: pure / effect / opaque
  • actual tracked reads
  • declared writes
  • transactional patches
  • effect receipts

Parallel same-path writes are rejected with a structured conflict explanation. Field permissions and budgets can reject work before side effects.

Evidence:

pnpm --dir packages/driver test
uv run --directory python/reactivegraph python -m pytest tests/test_observability.py -q

5. Migration-cost comparison

Supported ordinary LangGraph state graphs can be inspected and imported without executing node functions:

from reactivechain import inspect_langgraph, import_langgraph

report = inspect_langgraph(upstream_graph)
native_graph = import_langgraph(upstream_graph)

Conditional edges are explicitly reported unsupported; they are not silently converted with changed semantics.

Existing LangChain-compatible tools and messages can use optional adapters. ReactiveChain runtime has no LangChain dependency.

Evidence:

uv run --directory python/reactivechain \
  python -m pytest tests/test_langgraph_import.py tests/test_tools.py -q

6. Measured comparison of selective-execution mechanics

Selective execution exists on both sides, but the mechanism differs. Measured on this machine with the same 1000-node graph shape (independent nodes, one field changes between runs), using each library's documented API:

Configuration Nodes executed Notes
ReactiveGraph Driver (task declares reads) 1 / 1000 default behaviour
ReactiveGraph Python fallback (task declares reads) 1 / 1000 default behaviour
ReactiveGraph (no reads declared) 1000 / 1000 unscoped fallback, never under-skips
langgraph 1.2.11 + per-node CachePolicy(key_func=...) 1 / 1000 requires an explicit key per node
langgraph 1.2.11 + set_node_defaults(cache_policy=CachePolicy()) 1000 / 1000 measured: the global default does not skip
langgraph 1.2.11, no cache 1000 / 1000 default

What this shows, and what it does not:

  • Shows: the Driver's fingerprint granularity is available without writing a cache key per node; declaring reads (which a task already needs for conflict detection and scope isolation) is sufficient.
  • Does not show: that LangGraph is incapable of selective execution. It can skip with a correctly written key_func — it just requires that explicit per-node configuration, and the one-line set_node_defaults form measured above does not achieve it.
  • The 1000x figures elsewhere in this document come from that difference in configuration, not from LangGraph being architecturally unable to skip. Read them together with this table.

Reproduce: packages/driver/test/scheduler/reads-scoped-fingerprint.test.ts covers the ReactiveGraph rows; the LangGraph rows use the same graph shape with langgraph==1.2.11 pinned in benchmarks/differential/uv.lock.

7. Honest gaps

LangGraph and LangChain remain stronger in:

  • integration count
  • community size
  • public production deployments
  • documentation and examples
  • package maturity and installed base

ReactiveGraph's public release also requires repository-side secrets (NPM_TOKEN, PYPI_TOKEN) and an @reactivegraph npm organization. These are external operational requirements, not engineering claims.

The architectural lead is in the reactive runtime. Full ecosystem parity is not claimed.