Library / SDK
ReactiveX/rxjs avatar
ReactiveX/rxjs

RxJS 9: a platform Observable, Symbol-keyed operators, and an ESM-only build

A reactive programming library for JavaScript

31,697 stars2,985 forksTypeScriptApache-2.0

At a glance

What is it?
The master branch of ReactiveX/rxjs is no longer the library you install as rxjs today. It is a prerelease rewrite that delegates to the native web-platform Observable and exposes operators as Symbols. Here is what the repository documents, what it does not, and where the migration cost lands.
Who is it for?
RxJS 9 is not adoptable today: 9.0.0-beta.0 is not on npm and the next tag points at the earlier RxJS 8 prerelease, so production work belongs on RxJS 7. Start evaluating only if you are already on Node 22.13+ with pnpm 10.34.5 and can run pnpm --filter rxjs exec vitest --run src against a clone of master.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 38 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The problem RxJS 9 is solving is a naming and ownership problem

RxJS 7 ships its own Observable class and its own operator methods. That was fine while no Observable existed in the language, but the Web Platform Observable proposal has since been finalized, and a library that carries a second, incompatible Observable is a library that forces interop shims at every boundary. The README frames the rewrite around that: RxJS uses the native web-platform Observable when one exists and installs a conforming fallback only when needed. The audience is therefore narrow and specific. It is teams that already write RxJS, run on runtimes where the platform Observable is present or can be polyfilled, and are willing to absorb an API break to stop maintaining a parallel reactive type. It is not aimed at someone who wants a small event-emitter replacement, and it is not aimed at anyone who needs a stable release this quarter, because the README states that the planned first beta has not been published to npm yet.

Symbol-keyed operators replace string-named methods

The most consequential design choice is how operators attach. In RxJS 9, operators and factories are exact, module-owned Symbols, and the README is explicit that they do not add string-named RxJS methods to the platform API. The documented call form is bracket syntax, taken from the preview example: source[map]((value) => value * 2).subscribe(console.log), with map imported from the subpath rxjs/map rather than from the package root. The README draws the boundary sharply: a platform method such as observable.map(project) remains the platform contract, while observable[map](project) is the separately versioned RxJS contract. This is a real trade-off. It removes any chance of colliding with a future platform method of the same name, and it makes it impossible to monkey-patch an operator onto the prototype by string. It also means every call site reads differently from RxJS 7, and any code that relies on prototype extension, string lookups, or dynamic operator names by string has no direct equivalent described here.

Cold versus platform Observable is an explicit contract, not a default

RxJS 7 users are used to operators returning lazy Observables that create a fresh producer per subscription. The README states that in RxJS 9, platform Observable behavior and producer-per-subscription behavior are explicit, separate contracts, and instructs you to use ColdObservable when each direct subscription must create its own producer. The preview example constructs one directly: new ColdObservable<number>((subscriber) => { subscriber.next(1); subscriber.next(2); subscriber.complete(); }). The practical consequence is that laziness stops being an ambient property of the type and becomes a property of which class you instantiated. That is arguably clearer, but it also means a refactor that swaps a ColdObservable for a platform Observable can silently change how many times a producer runs. The README does not describe what happens to a ColdObservable that is subscribed twice, or how the two contracts interact inside a single operator chain, so that behaviour has to be read out of the source and the docs/rxjs-next design records rather than inferred from the README.

Cancellation moves to AbortSignal and the Subscriber lifecycle

The README states that cancellation is built on AbortSignal and the platform Subscriber lifecycle. For anyone who has wired up AbortController for fetch, the shape will be familiar, and it removes the need for a bespoke Subscription object that only RxJS understands. What the README does not state is how Subscription, the RxJS 7 teardown object, maps onto AbortSignal, whether unsubscribe() remains available, or how teardown functions registered inside a subscriber are ordered relative to an abort. Those are the questions that decide how mechanical a migration is, and the README does not answer them. The pointer given is packages/rxjs/MIGRATION.md, which is where the actual answers should be checked before planning any port.

Packaging: ESM only, no duplicate CommonJS build

Published JavaScript is ESM-only. The README notes that current Node can bridge require() to the same ESM files, and that there is no duplicate CommonJS build. That is a deliberate reduction in maintenance surface: one artifact instead of two, and the README claims that because every supported consumer receives the same ESM implementation, Deno and Bun support adds no runtime-specific package or application-bundle code. The cost lands on tools that cannot bridge require() to ESM. The README names Webpack 5 as a blocking environment, but it does not enumerate which bundlers or test runners fall outside that bridge, so if your build pipeline is not on the blocking list you are on your own until the release gates say otherwise.

Supported runtimes are narrow and gated

The planned beta supports Node 22.13+ and Node 24 as blocking lanes, with Node 26 in an advisory lane. Current Chrome, Firefox, desktop Safari, Mobile Safari, Deno, Bun, and Webpack 5 are blocking. Anything below Node 22.13 is outside the stated support window, which is a sharper cutoff than most libraries of this age carry. The repository also splits the codebase into four packages with distinct jobs: rxjs for Symbol extensions and intentional RxJS primitives, @rxjs/observable-polyfill for the conditional platform Observable fallback, @rxjs/test for implementation-neutral virtual-time and marble testing, and @rxjs/migrate for a deterministic migration engine plus what the README calls a canonical agent Skill. That last package is the interesting one for adopters, because it implies the project expects the RxJS 7 to 9 transition to be driven by tooling rather than by hand, though the README does not describe what the migration engine rewrites or how it handles the Symbol call form.

The RxJS 7 corpus is migration evidence, not a compatibility gate

One line in the README deserves to be read twice: the complete source-pinned RxJS 7 corpus intentionally retains reviewed lifecycle and compatibility divergences, so it is migration evidence rather than a blanket RxJS 9 compatibility gate. In plain terms, the project has a large body of RxJS 7 code it runs against the new implementation, and it has accepted that some of that code will not behave identically. Passing the corpus does not mean your RxJS 7 application will behave identically. The README points instead at focused source, package, runtime, browser, performance, and WPT commands as the release gates, and at packages/rxjs/docs/RELEASE_GATES.md for the exact environment gates and budgets. This is the single most important thing to understand before planning a port: the compatibility story is deliberately partial, and the divergences are reviewed rather than eliminated.

Getting a checkout running, and what the commands actually cover

Contributing requires Node 22.13+ and pnpm 10.34.5, and the README says to run commands from the repository root. The documented sequence is pnpm install, then pnpm --filter rxjs exec vitest --run src, then pnpm --filter rxjs run test:package, then pnpm run release:check. Note what these do and do not do. The vitest command is filtered to the rxjs package and scoped to src, so it is a unit-level run, not the full gate. test:package exercises the published package shape, which matters more than usual here because the package is ESM-only and the operator entry points are subpaths like rxjs/map. release:check is the aggregate. None of these commands installs RxJS 9 from npm, and the README warns directly against using the next tag to install RxJS 9 before 9.0.0-beta.0 is published, since next currently points at the earlier RxJS 8 prerelease. Until then, the only way to exercise the API is from a clone of master.

The alternative is to stay on RxJS 7, and the difference is not cosmetic

The realistic alternative is the RxJS 7 line, which the README identifies as the production latest line that continues to be maintained. The difference in approach is structural rather than incremental. RxJS 7 owns its Observable type and its operator methods; RxJS 9 does not, and instead layers Symbol-keyed extensions over whatever Observable the platform provides. That means RxJS 7 interops with other RxJS 7 code by default and with platform Observables through adaptation, while RxJS 9 is designed to interop with platform Observables natively and to treat RxJS-specific behaviour such as producer-per-subscription as an opt-in via ColdObservable. If your codebase is entirely RxJS 7 and has no platform Observable at its boundaries, the rewrite buys you an API break and a narrower runtime window in exchange for a boundary you do not currently have. The counter-case is a codebase that already passes platform Observables across library boundaries, where maintaining two Observable types is the larger cost. Note also that the RxJS 8 prerelease exists as a paused branch, and the README explains the version jump to 9 as a way to make the architectural break unmistakable rather than to present the old RxJS 8 work as the released product.

Maintenance model, licence, and what to verify

The repository is Apache-2.0, which permits commercial and closed-source use, and the README links the licence file rather than restating terms; that is a pointer, not legal advice, and any redistribution or patent question belongs with your own counsel. On maintenance, the README points to a security-assurance document that covers release evidence, verification commands, a sole-maintainer model, and an OpenSSF Scorecard in context. The phrase sole-maintainer model is the one to weigh. A single maintainer is not disqualifying for a library with this much downstream use, but it does mean the release cadence and the security response time are one person's, and the README is honest enough to put that in writing next to the verification commands instead of leaving it implicit. The upgrade cost itself is the least documented part of the README. The README gives a migration guide path and a migration engine package, but no estimate of how much RxJS 7 code survives unchanged, and the corpus caveat above means you should assume some of it does not. Verify three things before you commit: whether 9.0.0-beta.0 has actually been published and what the next tag points at on that day, whether your bundler and test runner are on the blocking list in RELEASE_GATES.md, and how your specific operator chains behave under the ColdObservable versus platform Observable split, since that is the contract the README flags as explicit and separate.

Editorial conclusion

RxJS 9 is not adoptable today: 9.0.0-beta.0 is not on npm and the next tag points at the earlier RxJS 8 prerelease, so production work belongs on RxJS 7. Start evaluating only if you are already on Node 22.13+ with pnpm 10.34.5 and can run pnpm --filter rxjs exec vitest --run src against a clone of master. Before committing to a migration, read packages/rxjs/MIGRATION.md and packages/rxjs/docs/RELEASE_GATES.md, and treat the retained RxJS 7 corpus divergences as evidence rather than as a compatibility guarantee.

Official sources

  1. License: Apache-2.0
  2. Project website
  3. ReactiveX/rxjs on GitHub
  4. README
  5. Releases
Community notes

Community notes