Skip to content

ActualLab.FusionThe shortest path to real-time UI.

Add real-time updates and caching to any .NET app with almost no code changes. Get 10⁢ scale headroom. Production-proven. MIT license.

BuildNuGet VersionCommit ActivityDownloadsChat @ Voxt

See It In Action ​

The Problem ​

Building real-time apps is hard. Traditional approaches force you into painful trade-offs:

  • 🐒 No cache = slow UI. But caching brings the invalidation problem. Miss one case and users get stuck seeing stale data.
  • πŸ“š Real-time = a lot of extra code. Design an update notification protocol, ensure UI subscribes only to relevant updates, apply them so the UI state stays eventually consistent with the ground truth... And that's just the client side!
  • 🀯 Complexity multiplies. Each data type needs its own subscription groups, update messages, and client-side handlers. Reconnection? Re-negotiate everything, reconcile state. What starts as "just add SignalR" becomes thousands of lines of infrastructure.
  • πŸŒ‹ Platform-specific code multiplies it further. We pair .NET servers with JS and mobile clients, all sharing the same data and the same complex logic for caching and real-time updates.

But if you think about it, caching and real-time updates are facets of the same problem. Both require knowing when something changes and who cares. Yet we treat them as separate concerns with separate infrastructure.

Fusion solves all of this:

  • πŸͺ„ Tracks dependencies automatically
  • 🎯 Invalidates precisely what it should
  • πŸ“‘ Propagates invalidations to everyone who cares, including remote clients
  • πŸ€— Works identically everywhere, turning your server farm, mobile apps, and web clients into a single distributed dependency graph.

The best part: you get all of this without turning your code into a mess. You can think of Fusion as a call middleware or a decorator. That's why Fusion-based code looks as if there is no Fusion at all! So you can focus on building your app and ship faster β€” and save yourself from dealing with a 2–3Γ— larger codebase and a plethora of "why is it stale?" bugs, which are among the hardest to debug.

Performance That Changes Everything ​

Fusion doesn't just add real-timeβ€”it makes your app thousands of times faster.

Fusion Compute Services ​

ScenarioWithout FusionWith FusionSpeedup
Local service, minimal writes38.6K calls/s313.8M calls/s8,127x
Local service, continuous writes135.4K calls/s266.6M calls/s1,968x
Remote service, continuous writes100.7K calls/s (REST)226.7M calls/s2,251x

Benchmarks on AMD Ryzen 9 9950X3D. See full benchmark details.

ActualLab.Rpc vs Alternatives ​

FrameworkRPC Calls/secStreaming Items/sec
ActualLab.Rpc9.33M101.17M
SignalR5.30M17.17M
gRPC1.11M39.59M
Speedup1.8..8.4x2.6..5.9x

8.4x faster than gRPC for calls. 5.9x faster than SignalR for streaming.

How It Works: The MSBuild/Make Analogy ​

Think of Fusion as MSBuild for data processed by your backend, API, and even client-side UI:

  • Targets = Method calls like GetUser(userId)
  • Artifacts = Method call results (cached values)
  • Dependencies = Other method call results acquired during method execution
  • Incremental builds = When you request a result, only outdated parts recompute

When GetThumbnail(imgId, 64) is invalidated, the invalidation cascades immediately:

Next request for GetUserProfile(3) triggers partial recomputation β€” only the affected parts recompute, while GetUser(3) is served from cache:

The invalidation is always immediate and cascading: when you invalidate a given call, its dependency sub-graph is also invalidated, including remote dependencies.

But invalidation doesn't imply immediate recomputation: the recomputation typically happens later, when the call is repeated, typically in a UI component. But old cached values wrapped into Computed<T> instances remain accessible indefinitely, so UI can keep displaying them as long as it needs to (while updates are in progress or even later).

Fusion tracks one Computed<T> per each (service, method, arguments) combination in a WeakMap-style structure β€” invalidation evicts the entry, so the next call recomputes it, while unrelated entries stay cached:

The dependency graph updates automatically as your methods call each other or when invalidation occurs, so typically you don't even need to know it exists.

This is exactly how incremental builds work: you mark targets as dirty by removing them, but they only rebuild when you run the build, and every artifact that's still consistent is reused.

The Distributed Picture ​

The same dependency graph extends across network boundaries β€” invalidation cascades from backend to client, then recomputation flows back from client to backend, reusing every node that's still consistent:

When multiple clients use multiple servers, the reuse rate of already-computed results is extremely high. Typically only the first client requesting data after an invalidation triggers actual computation β€” nearly everyone else gets a cache hit:

In production, the dependency graph is orders of magnitude larger. Here are real numbers from Voxt β€” a Fusion-based app:

MetricValue
Computed instances per client5–10K
Remote dependencies per client1–2K
Time from app start to first contact list render0.5s

Having thousands of remote dependencies doesn't slow down startup β€” they resolve from the local cache first and update as soon as RPC updates arrive.

A typical backend invalidation touches just a few nodes on each client β€” only a tiny fraction of the graph is ever recomputed per change.

See The Code ​

A Fusion service looks almost identical to a regular service:

csharp
public class UserService(IServiceProvider services) : DbServiceBase<AppDbContext>(services), 
    IComputeService // A tagging interface that enables [ComputeMethod] and other Fusion features
{
    [ComputeMethod] // Also has to be virtual
    public virtual async Task<User?> GetUser(long id, CancellationToken cancellationToken = default)
    {
        // Fusion services are thread-safe by default, but DbContext is not, 
        // so we can't use shared DbContext instance here.
        // That's why we use DbHub, which provides DbContext-s on demand and pools them.
        await using var dbContext = await DbHub.CreateDbContext(cancellationToken);
        return await dbContext.Users.FindAsync([id], cancellationToken);
    }

    [ComputeMethod] // Also has to be virtual
    public virtual async Task<UserProfile> GetUserProfile(long id, CancellationToken cancellationToken = default)
    {
        // Calls other compute methods - dependencies tracked automatically
        var user = await GetUser(id, cancellationToken);
        var avatar = await GetUserAvatar(id, cancellationToken);
        return new UserProfile(user, avatar);
    }

    // Regular method
    public async Task UpdateUser(long id, User update, CancellationToken cancellationToken = default)
    {
        await using var dbContext = await DbHub.CreateDbContext(readWrite: true, cancellationToken);
        var user = await dbContext.Users.FindAsync([id], cancellationToken);
        user!.ApplyUpdate(update);
        await dbContext.SaveChangesAsync(cancellationToken);

        using (Invalidation.Begin()) { // Invalidation block
            _ = GetUser(id, default); // Invalidate GetUser(id), this call completes synchronously w/o actual evaluation
        }
    }
}

That's it. No event buses. No cache managers. No subscription tracking.

Real-Time UI in Blazor ​

Fusion provides ComputedStateComponent<T>, which has a State property β€” a ComputedState<T> instance that holds the latest result of the ComputeState call. Any ComputedState<T> is essentially a compute method + update loop, so it invalidates when any dependency of its last computation gets invalidated, and recomputes after a short delay (configurable via GetStateOptions).

When State gets recomputed, StateHasChanged() is called and the component re-renders.

razor
@inherits ComputedStateComponent<UserProfile>
@inject IUserService UserService

<div class="profile">
    <h1>@State.Value.Name</h1>
    <p>@State.Value.Bio</p>
    <span>Posts: @State.Value.PostCount</span>
</div>

@code {
    [Parameter] public long UserId { get; set; }

    protected override Task<UserProfile> ComputeState()
        => UserService.GetUserProfile(UserId);
}

Why Developers Choose Fusion ​

πŸš€ Ship Faster ​

Skip building real-time and caching infrastructure. Add [ComputeMethod] to your existing services and get both for free.

🌱 Start Small, Go Big ​

Same service code works for a single Blazor app or a distributed cluster. Going from prototype to planet-scale is almost a flip of a switch.

πŸ”οΈ 10⁢ Scale Headroom ​

With 10,000x faster services, just 100 sharded servers give you a million-fold scale headroom.

πŸ› Fewer Bugs ​

No more "it's stale β€” find out why" debugging sessions. Automatic dependency tracking ensures dependents update when something changes.

πŸ’» No (Micro)Service Zoo ​

Your services run locally or distributed with zero changes. No complex dependencies. AI agents can debug your code by running E2E tests right on your laptop. Or in Docker.

πŸ’Ž Clean Code ​

Your code stays focused on business logic, Fusion handles the rest. Forget about the boilerplate for real-time updates or cache invalidation.

Built & Battle-Tested at Voxt.ai ​

Voxt is a real-time chat app built by the creators of Fusion. It features:

  • πŸŽ™οΈ Real-time audio with πŸ”€ live transcription, 🌐 translation, πŸ“ AI summaries, and much more
  • πŸ“± Clients for WebAssembly, iOS, Android, and Windows
  • πŸ’° ~100% code sharing across all platforms
  • ✈️ Offline mode powered by Fusion's persistent caching.

Check out how it works at Voxt.ai, or reach out to Alex Y. @ Voxt.ai if you want to chat in real time. Fusion handles everything related to real-time there.

Get Started in Minutes ​

1. Install the Package ​

bash
dotnet add package ActualLab.Fusion

2. Register Your Services ​

csharp
services.AddFusion().AddService<UserService>(); // UserService must "implement" tagging IComputeService

3. Add [ComputeMethod] to Your Methods ​

csharp
[ComputeMethod]
public virtual async Task<User> GetUser(long id) { ... }

That's the entire setup. Your service now has automatic caching, dependency tracking, and real-time invalidation.

Join the Community ​

Questions? Want to see how others use Fusion? Join the discussion:

Credits ​

Indirect contributors & everyone else who made Fusion possible:

ActualLab.Fusion is developed by the creators of Voxt and is the successor of Stl.Fusion, originally created at ServiceTitan. Check out The Story Behind Fusion to learn more about its origins.