← All Quick Lessons
ASP.NET Core Responsiveness13 min read · 18 August 2026

Responsive ASP.NET Core Applications: Do Less Work and Keep Users Moving

Improve application responsiveness by shortening the critical path, caching safely, virtualising large data sets, using SignalR appropriately and moving reliable background work out of the user's wait.

A responsive application does more than complete operations quickly. It keeps the user moving and avoids making them wait for work that does not belong in the critical path.

In practice, that can mean using asynchronous I/O, caching stable data, returning smaller responses, rendering only visible content, moving long-running work into a background process or pushing updates instead of repeatedly polling for them.

1. Responsive does not simply mean fast

Imagine that an application needs three seconds to generate a report. One design freezes the page until everything completes. Another accepts the request, shows clear progress and lets the user continue while the report is prepared. The work may take the same time, but the second application feels better because the user remains informed and in control.

Responsiveness has two parts: actual responsiveness reduces the time and resources required, while perceived responsiveness acknowledges the user's action and communicates progress clearly. Good engineering considers both.

2. Keep I/O asynchronous

csharp
private async Task<Customer?> LoadCustomerAsync(
    int id,
    CancellationToken cancellationToken)
{
    return await _customerService.GetCustomerAsync(
        id,
        cancellationToken);
}

An asynchronous call does not make the external system physically faster. It prevents the application from unnecessarily blocking a thread while waiting. The async path should continue through the full I/O call chain, and cancellation tokens should reach operations that support them so abandoned work can stop.

Endpoint
    ↓ await
Application service
    ↓ await
Repository
    ↓ await
Database or remote API

3. Keep the critical path small

The critical path is the work that must finish before the user can receive a useful response. Saving an order probably belongs there; sending email, updating analytics or generating an export may not.

Validate and save order
      ↓
Queue follow-up work
      ↓
Return confirmation
      ↓
Background worker sends email
and performs secondary tasks

Avoid unmanaged fire-and-forget work inside a request. The request may finish while an untracked task is running, and shutdown, exceptions or scoped dependencies can make it unreliable. Use a durable queue, message broker or managed background-service pattern when the work matters.

Code-review question

Must this operation finish before the user receives a useful and truthful response?

4. Cache work that does not need to be repeated

csharp
public async Task<IReadOnlyList<Country>> GetCountriesAsync(
    CancellationToken cancellationToken)
{
    return await _cache.GetOrCreateAsync(
        "countries",
        async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow =
                TimeSpan.FromHours(1);

            return await _database.GetCountriesAsync(
                cancellationToken);
        }) ?? [];
}

The application checks the cache first. If the value is absent, it retrieves the countries and stores them. Caching can reduce database queries, remote calls, repeated calculations, serialisation work, response time and pressure on downstream services.

5. Every cache needs a freshness policy

Code-review question

How stale can this information safely become?

A one-hour cache may suit country codes but be unacceptable for account balances, stock levels or permissions. Decide how long a value remains valid, what invalidates it, whether stale data is acceptable during failure, whether it is user-specific, and how concurrent misses avoid rebuilding it repeatedly.

Expiration is a business decision about freshness. Sensitive or personalised responses also require careful keys and policies: a cache must never return one user's protected data to another.

6. Choose the cache that matches the deployment

Server A → its own memory cache
Server B → its own memory cache
Server C → its own memory cache

IMemoryCache belongs to one running instance. It can suit a single server or data where each instance may safely hold a different copy. A distributed cache such as Redis may be appropriate when multiple instances need shared cached data.

HybridCache provides a higher-level API that can combine fast in-process caching with distributed storage. It also includes stampede protection so many concurrent requests do not all execute the same expensive cache-miss operation. You still need an explicit consistency and invalidation strategy.

7. Output caching avoids more than a database call

csharp
app.MapGet(
        "/products",
        async (
            ProductService service,
            CancellationToken cancellationToken) =>
        {
            return await service.GetProductsAsync(
                cancellationToken);
        })
    .CacheOutput();

Data caching stores information used to build a response. Output caching stores a suitable generated HTTP response, potentially avoiding the endpoint, service, database and serialisation work on a later matching request.

Output caching can benefit stable, frequently requested responses, but it must respect route values, query strings, headers, authentication and freshness rules. Do not cache an endpoint merely because it is slow; establish whether its response is safe and meaningful to reuse.

8. Render only what the user can see

razor
<Virtualize Items="transactions"
            Context="transaction">
    <TransactionRow Transaction="transaction" />
</Virtualize>

If a Blazor page represents 100,000 transactions while the viewport shows 20, rendering every row creates work the user cannot see. Virtualize<TItem> limits rendering to the visible region and a small surrounding area. An items provider can retrieve only the required range.

Virtualisation does not automatically repair an inefficient query. If the app still downloads all 100,000 rows before showing 20, it solves only part of the problem. Combine server-side filtering, pagination or range retrieval, smaller DTOs and UI virtualisation.

9. Avoid polling when the server already knows what changed

Polling repeatedly asks whether anything changed. It can be reasonable when updates are infrequent and delay is acceptable, but frequent polling may create many requests that return nothing useful. A WebSocket creates a longer-lived two-way channel so the server can send an update when an event occurs.

A value changes
      ↓
Server pushes an update
      ↓
Connected client refreshes

This can suit live dashboards, chat, notifications, collaboration, tracking and operation progress. The choice should depend on update frequency, scale, infrastructure and reliability requirements—not an assumption that real-time is always better.

10. SignalR provides a higher-level real-time model

csharp
public sealed class NotificationHub : Hub
{
    public async Task SendMessage(string message)
    {
        await Clients.All.SendAsync(
            "ReceiveMessage",
            message);
    }
}

ASP.NET Core SignalR supplies hubs and client APIs above raw WebSockets. The hub is a server-side communication point; Clients.All selects connected clients and SendAsync invokes their named handler with the data.

Real-time connections consume server and network resources, and multi-server deployment needs an appropriate scale-out design. Azure SignalR Service can help manage large connection counts. Use real-time delivery when prompt updates benefit the user, not merely because the technology exists.

11. Keep the user interface responsive

csharp
private async void LoadButton_Click(
    object sender,
    EventArgs e)
{
    LoadButton.Enabled = false;

    try
    {
        var customers =
            await _service.GetCustomersAsync();

        CustomerGrid.DataSource = customers;
    }
    finally
    {
        LoadButton.Enabled = true;
    }
}

Async void is normally avoided because callers cannot await it, but UI event handlers are a recognised exception. Genuine asynchronous I/O lets the interface repaint and handle interaction while waiting. The finally block restores the button even when the operation fails.

A responsive UI gives immediate acknowledgement, shows loading or progress, prevents accidental duplicate submission, offers cancellation for long work, reports errors clearly and restores controls after completion or failure. A spinner cannot repair poor architecture, but silence makes reasonable waiting feel broken.

12. Do not confuse responsiveness with hiding failure

Moving work out of a request does not make it optional. If an API responds before background work completes, its status should tell the truth: 200 OK means the requested operation completed, while 202 Accepted means the work was accepted and will continue.

json
{
  "operationId": "report-8472",
  "status": "queued"
}

For long-running work, return an operation identifier and expose progress through a status endpoint or SignalR. Responsive design should shorten unnecessary waiting while making the true state visible.

A practical responsiveness review checklist

  • Critical path: What must finish before the user can receive a useful response?
  • Blocking: Is the application blocking while waiting for I/O?
  • Cancellation: Can abandoned work be stopped?
  • Repeated work: Are we retrieving or calculating the same result repeatedly?
  • Caching: Is the result safe to cache, and how stale may it become?
  • Deployment: Is the cache local to one server or shared across instances?
  • Payload: Are we returning more data than the user currently needs?
  • Rendering: Are we rendering elements outside the visible viewport?
  • Polling: Are clients repeatedly asking for changes the server could push?
  • Background work: Can non-essential work leave the critical path safely?
  • Reliability: Is queued work durable, observable and retryable?
  • Feedback: Does the interface acknowledge progress, completion and failure?
  • Truthfulness: Is the work finished or merely accepted?

The responsiveness model to remember

Waiting for I/O
      ↓
Use genuine async APIs and cancellation

Repeated stable work
      ↓
Cache with an explicit freshness policy

Reusable HTTP response
      ↓
Consider output caching

Large data set
      ↓
Filter, page and virtualise

Frequent polling
      ↓
Consider WebSockets or SignalR

Long non-essential follow-up work
      ↓
Queue it reliably outside the critical path

Long user-visible operation
      ↓
Show progress and report the true status

A responsive application does not merely hide waiting. It removes avoidable work, keeps the critical path focused, communicates progress and lets the user continue wherever the business process allows it.

ASP.NET Core performanceresponsive applicationsIMemoryCacheHybridCacheoutput cachingBlazor virtualizationSignalRWebSocketsbackground processingapplication responsiveness

Want to go deeper?

Continue with the detailed C# and .NET performance guides in the Journal.