Cysharp/R3: A Modern Reactive Extensions Reimplementation for .NET and Unity
The new future of dotnet/reactive and UniRx.
At a glance
- What is it?
- R3 replaces IObservable<T> and IObserver<T> with abstract classes, drops IScheduler, and keeps pipelines alive after an exception. Here is what that means for a .NET or Unity codebase.
- Who is it for?
- Adopt R3 if you are starting a new Unity, Godot or desktop .NET project and want an Rx whose error model does not tear down the subscription, or if you have hit allocation pressure from Subject subscriptions in dotnet/reactive. Do not adopt it if you depend on existing IObservable<T> interop, on IScheduler-based virtual time testing, or on the Rx.NET operator surface you already know, because R3 changes the core interfaces and some operators are absent.
- Can I use it commercially?
- Yes. MIT 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 35 days ago.
- What is it written in?
- Mainly C#, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Why Cysharp rewrote Rx instead of extending it
R3 is a from-scratch Reactive Extensions implementation for .NET, positioned by its author as the successor to both dotnet/reactive and UniRx. The README is unusually blunt about the motivation: it lists design decisions the author considers mistakes in the existing libraries. Stopping the pipeline at OnError is called a mistake. IScheduler is called the root of poor performance. Frame-based operations are described as a missing feature that matters especially in game engines. Synchronous APIs, query syntax outside SQL, and backpressure are all declared out of scope, with backpressure explicitly delegated to IAsyncEnumerable and Channels.
The audience follows from that list. This is not a general-purpose reactive library for server-side stream processing, and the author says so: the stated focus is in-memory messaging, LINQ to Events, not communication protocols like Reactive Streams. If you write Unity gameplay code, Godot nodes, or desktop UI in WPF, Avalonia, WinForms, WinUI3 or MAUI, you are the intended reader. The README also names Stride, LogicLooper, MonoGame, Blazor and Uno as supported platforms.
Observable<T> and Observer<T> as abstract classes, and what OnErrorResume changes
The core interfaces are different from standard Rx, and the README is explicit that R3 does not use IObservable<T> or IObserver<T>. Both are abstract classes instead. Observable<T> exposes a single Subscribe(Observer<T> observer) method returning IDisposable. Observer<T> exposes OnNext(T value), OnErrorResume(Exception error), and OnCompleted(Result result), where Result is a readonly struct with two states, Success and Failure.
The behavioural difference is the important part. In dotnet/reactive, an exception in the pipeline flows to OnError and the subscription is torn down. In R3 it flows to OnErrorResume and the subscription stays alive. The README argues this is the right default for event handling, that resolving it inside an operator like Retry is difficult and risky, and that converting OnErrorResume into OnCompleted(Result.Failure) is easy while the reverse is impossible. That asymmetry is the whole argument: the library picks the direction that can be undone.
The second change is the collapse of the two terminal signals into one. The classic OnError | OnCompleted contract becomes a single OnCompleted(Result result). The stated reason for using abstract classes rather than interfaces is that Rx carries implicit contracts an interface cannot enforce, so making them classes let the author control the behaviour of Subscribe.
There is a real cost here that the README does not dwell on. Any code, library or test helper that speaks IObservable<T> needs an adapter, and the README does not document one in the text available. For a greenfield Unity project that is irrelevant. For a codebase with existing Rx plumbing it is the first thing to check.
Installing R3 from NuGet and writing a first pipeline
The library ships as the R3 package on NuGet and supports .NET Standard 2.0, .NET Standard 2.1, .NET 6, .NET 7 and .NET 8 or above. The README gives a single install command.
dotnet add package R3Platforms such as WPF, Avalonia, Unity and Godot need an additional step, which the README points to in its Platform Supports section rather than spelling out in the install block, so read that section before wiring anything up.
The README's own first example builds a pipeline from Observable.Interval, filters it with Select and Where, and subscribes. The chaining style is LINQ methods, which is why the README says existing Rx knowledge and documentation transfer almost directly.
using R3;
var subscription = Observable.Interval(TimeSpan.FromSeconds(1))
.Select((_, i) => i)
.Where(x => x % 2 == 0)
.Subscribe(x => Console.WriteLine($"Interval:{x}"));The same example then shows a second pipeline built from Observable.Timer with a TakeUntil bound to a CancellationToken, consumed through ForEachAsync, and finished with subscription.Dispose(). What you should see when you run it is a line per even tick from the interval pipeline while the timer pipeline runs alongside it. The two mechanisms, IDisposable from Subscribe and a CancellationToken from TakeUntil, are both first-class here, and the README presents them together rather than choosing one.
Subject allocations and the IScheduler removal
The README makes two performance claims and shows a benchmark image for each, without publishing numbers in the text. The first image is labelled Observable.Range(1, 10000).Subscribe() and is used to illustrate what the author calls the terrible performance of IScheduler and the difference caused by removing it. R3 has no IScheduler; scheduling concerns are left to the platform and to async/await.
The second is about Subject. The README states that dotnet/reactive adopted ImmutableArray or an equivalent for Subject, so every subscribe and every dispose allocates a new array. The accompanying image is labelled x10000 subject.Subscribe() -> x10000 subscription.Dispose(). The argument is that games in particular can accumulate large numbers of subscriptions, and that per-operation array allocation becomes a real cost at that scale. R3 states it devised a way to avoid ImmutableArray while keeping performance high, but the README does not describe the data structure, so treat the mechanism as undocumented and the claim as the author's.
There is also a debugging feature the README lists among the design goals: a subscription list to prevent subscription leaks, compared to a parallel debugger. The README does not give the API for it in the text available, so if leak detection is why you are looking at R3, confirm it exists in the docs directory before you plan around it.
Where R3 is the wrong tool
The README rules out several use cases on purpose, and taking those exclusions seriously saves time. Backpressure is not implemented; the author directs you to IAsyncEnumerable and Channels instead. Distributed processing and query workloads are directed to GraphQL, Kubernetes, Orleans, Akka.NET, gRPC and MagicOnion. If your problem is streaming data between services, R3 is not the answer and does not pretend to be.
Synchronous APIs are deliberately absent, so any code path that expects to pull values synchronously from an observable will not find it. Query syntax is rejected as a notation outside SQL, which means no query-comprehension style over observables.
The error model is the subtler trap. Because OnErrorResume does not unsubscribe, a pipeline that keeps throwing keeps running. That is the intended design, but it shifts responsibility to you: if you want classic fail-fast behaviour you must convert to OnCompleted(Result.Failure) yourself, and the README does not document a rollback or a global switch for that. Teams used to OnError terminating everything should plan for the change rather than discover it in production.
Finally, the interface change means R3 is not a drop-in replacement. The README says the surface API remains the same as normal Rx while the interfaces used internally are different, which is a precise statement: your operator chains look familiar, your types do not.
R3 against dotnet/reactive and UniRx
The honest comparison is with dotnet/reactive, since UniRx is the author's own earlier library and R3 is presented as its successor. The difference is not operator count. It is the contract.
dotnet/reactive keeps IObservable<T> and IObserver<T>, keeps IScheduler as the scheduling abstraction, and terminates a subscription on OnError. That design is mature, widely documented, and interoperates with everything else in the .NET ecosystem that speaks IObservable<T>. R3 trades that interoperability for a different error model, no scheduler abstraction, and a Subject implementation the author says avoids per-subscription array allocation. The README's own framing is that IObservable<T> as the dual of IEnumerable<T> is a beautiful definition but not very practical in use.
UniRx occupies a different position: it was built for Unity before modern C# and before async/await were practical in that environment. R3's stated rationale is that C# has moved on, and that Rx-like frameworks optimized for current language features, such as Kotlin Flow and Swift Combine, have become the norm. If you are on UniRx today, R3 is the migration path the author intends, and the README notes your Rx knowledge transfers; the interfaces do not.
Licence, releases and what maintenance looks like
R3 is MIT licensed, with the LICENSE file at the repository root. MIT is permissive: it allows commercial and closed-source use with attribution and no warranty, which matters for Unity projects shipping to stores. This is a description of the licence text, not legal advice; read LICENSE and your own counsel's guidance for anything consequential.
The repository is not archived, and the last push was on 2026-08-26, which is within the last month. Releases are tagged, and the cadence visible in the repository is uneven: 1.2.9 in September 2024, 1.3.0 in February 2025, and 1.3.1 in May 2026. Note that the 1.3.0 tag is spelled v.1.3.0 and the 1.2.9 tag is spelled Ver.1.2.9, so any tooling that parses tags by pattern will need to tolerate that inconsistency.
Upgrade cost is where a reactive library earns or loses its keep. Because R3 changes the core interfaces, an upgrade that touches Observable<T> or Observer<T> can ripple through your own operator and adapter code, not just through call sites. The README does not document a migration guide or a breaking-change policy in the text available, so pin the version in your project file and read the release notes for 1.3.1 before moving.
Editorial conclusion
Adopt R3 if you are starting a new Unity, Godot or desktop .NET project and want an Rx whose error model does not tear down the subscription, or if you have hit allocation pressure from Subject subscriptions in dotnet/reactive. Do not adopt it if you depend on existing IObservable<T> interop, on IScheduler-based virtual time testing, or on the Rx.NET operator surface you already know, because R3 changes the core interfaces and some operators are absent. Before committing, verify two things: that your target platform is listed in the Platform Supports section and its extra install step, and that every operator your code uses exists in R3, since the README says the surface API stays familiar but the internals do not.
Frequently asked questions
How do I install R3 in a .NET project?
Add the R3 package from NuGet. The README gives the command dotnet add package R3, and states the package supports .NET Standard 2.0, .NET Standard 2.1, .NET 6, .NET 7 and .NET 8 or above.
Does R3 use IObservable<T> and IObserver<T> like standard Rx?
No. R3 defines Observable<T> and Observer<T> as abstract classes instead of interfaces, which the README says was done so the library could fully control the behaviour of Subscribe. The README notes the surface API stays the same as normal Rx while the internal interfaces differ.
What happens in R3 when an exception occurs in the pipeline?
It flows to OnErrorResume and the subscription is not unsubscribed, unlike dotnet/reactive where OnError terminates the subscription. The README states this is intentional and that converting OnErrorResume to OnCompleted(Result.Failure) is possible if you want the pipeline to stop.
Does R3 support Unity, Godot and other platforms?
Yes. The README lists Unity, Godot, Avalonia, WPF, WinForms, WinUI3, Stride, LogicLooper, MAUI, MonoGame, Blazor and Uno as supported platforms, and notes that platforms such as WPF, Avalonia, Unity and Godot require an additional install step described in the Platform Supports section.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/cysharp-r3)