Skip to content

ActualLab.FusionThe "real-time on!" switch for .NET

Add real-time updates and caching to any .NET app with almost no code changes. 10,000x faster APIs. Production-proven.

BuildNuGet VersionCommit ActivityDownloadsSamplesChat @ 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 writes136.9K calls/s263.6M calls/s1,926x
Remote service, continuous writes99.7K calls/s (REST)223.2M calls/s2,239x

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

ActualLab.Rpc vs Alternatives ​

FrameworkRPC Calls/secStreaming Items/sec
ActualLab.Rpc8.87M95.10M
SignalR5.34M17.11M
gRPC1.11M38.75M

8x faster than gRPC for calls. 5.6x 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
GetUserProfile(3)
    β”œβ”€β”€calls──► GetUser(3)
    └──calls──► GetUserAvatar(3)
                    └──calls──► GetThumbnail("user_3_avatar", 64)

When GetThumbnail(imgId, 64) is invalidated:
  - GetUserAvatar(3) is immediately marked as inconsistent
  - GetUserProfile(3) is immediately marked as inconsistent
  - Nothing recomputes yet.

Next request for GetUserProfile(3) triggers recomputation of:
  - GetUserAvatar(3)
  - GetThumbnail("user_3_avatar", 64)
  
As for GetUser(3), it won't be recomputed when GetUserProfile(3) calls it,
because it wasn't affected by GetThumbnail("user_3_avatar", 64) invalidation,
so its cached value is going to be used.

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).

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.

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);
}

Production-Proven ​

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.

Try Voxt β†’

Why Developers Choose Fusion ​

πŸš€ Ship Faster ​

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

πŸ› Fewer Bugs ​

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

πŸ’Ž Clean Code ​

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

πŸ“ˆ Scale Effortlessly ​

Handle 1000Γ— more traffic with almost no changes to your code.

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:

  • ServiceTitan – Fusion was originally created there.
  • Quora – a huge part of the inspiration for Fusion was Quora's LiveNode framework
  • Microsoft – for .NET Core and Blazor.

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.