Improve ASP.NET Core Web API Performance and Resilience
Why APIs Matter (and Why They Often Feel Slow)
Web APIs are the backbone of most modern software systems. They connect mobile apps, web frontends, internal services, third-party integrations, and automation pipelines. If your API is fast and reliable, everything built on top of it feels responsive. If your API is slow or unstable, the entire product feels broken, no matter how good the UI is.
The tricky part is that API performance and resilience rarely fail because of one “big mistake.” In real projects, they degrade over time due to small decisions that seem harmless: one extra query here, one middleware there, one “temporary” synchronous call, one payload that grows without anyone noticing.
Minimal APIs: Great for Shipping, Easy to Misuse
Since .NET 6, Minimal APIs have become a popular way to ship endpoints quickly with less boilerplate. And honestly, they’re great: clean syntax, fewer layers, and a nice fit for smaller services or focused APIs.
But Minimal APIs also make it easier to accidentally:
- put too much logic inside endpoints,
- skip structure (DTOs, validation, consistent errors),
- forget observability (logging, tracing, metrics),
- build something that works in development but collapses under real traffic.
The point is not “Minimal APIs are bad.” The point is that the faster you can build, the easier it is to carry technical debt into production without noticing.
The Problems I’ve Seen Repeatedly in Real .NET APIs
Across different teams and projects, the same categories of issues show up again and again:
1) Inefficient data access
APIs that are “slow” are often just “waiting on the database.” Unindexed columns, N+1 queries, over-fetching, missing projections, heavy joins, or no caching strategy at all.
2) Serialization and payload bloat
People underestimate how expensive it is to serialize large object graphs, or how much bandwidth matters. Returning full entities, deep nesting, or fields nobody uses is a silent performance killer.
3) Middleware and pipeline overload
A long pipeline of logging, auth, mapping, exception handling, compression, swagger, custom stuff… is fine if each piece is justified. But many APIs accumulate middleware like plugins, and latency grows request after request.
4) Blocking and resource-heavy operations
Sync-over-async patterns, blocking calls, long CPU work inside request threads, unbounded parallelism, or missing timeouts. These don’t just slow down one request—they reduce throughput for everyone.
5) Missing resilience under real-world conditions
Production means timeouts, retries, flaky dependencies, and traffic spikes. Without rate limiting, circuit breakers, proper HttpClient usage, and sensible timeouts, APIs will fail in ways that are hard to reproduce.
Technique 1: Caching Strategies
Caching is often the fastest way to improve API performance with minimal code changes. In many real-world APIs, the same data is requested repeatedly, yet the system recalculates or re-queries it every time. Caching allows you to serve frequent requests quickly while reducing database and dependency load.
In-Memory Caching
In-memory caching is ideal when your API runs on a single instance or when cached data does not need to be shared across multiple nodes.
builder.Services.AddMemoryCache();
app.MapGet("/products/{id}", async (int id, IMemoryCache cache, ProductService service) =>
{
var cacheKey = $"product-{id}";
if (!cache.TryGetValue(cacheKey, out ProductDto product))
{
product = await service.GetProductAsync(id);
cache.Set(cacheKey, product, TimeSpan.FromMinutes(5));
}
return Results.Ok(product);
});
Best practices
- Cache DTOs, not EF entities.
- Use short, explicit expiration times.
- Use deterministic cache keys.
Common pitfalls
- Caching mutable objects.
- Forgetting cache invalidation on updates.
- Using in-memory cache in multi-instance environments.
Distributed Caching (Redis)
When running multiple instances or containers, distributed caching ensures consistency across nodes.
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
app.MapGet("/orders/{id}", async (
int id,
IDistributedCache cache,
OrderService service) =>
{
var cacheKey = $"order-{id}";
var cached = await cache.GetStringAsync(cacheKey);
if (cached is not null)
return Results.Ok(JsonSerializer.Deserialize<OrderDto>(cached));
var order = await service.GetOrderAsync(id);
await cache.SetStringAsync(
cacheKey,
JsonSerializer.Serialize(order),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
});
return Results.Ok(order);
});
Best practices
- Cache serialized data to keep cache storage simple.
- Prefer absolute expirations for distributed caches.
- Monitor cache hit ratio.
Common pitfalls
- Treating Redis as a primary data store.
- No fallback strategy when cache is unavailable.
- Over-caching large payloads.
Technique 2: Asynchronous Programming
Asynchronous APIs scale better because threads are not blocked waiting for I/O. Many performance problems in .NET APIs are not caused by slow code, but by blocked threads that reduce overall throughput.
Async programming is also closely related to request cancellation: if a client disconnects or times out, the server should stop unnecessary work as early as possible.
Use async/await for I/O-bound work (with cancellation)
app.MapGet("/users/{id}", async (
int id,
UserService service,
CancellationToken ct) =>
{
var user = await service.GetUserAsync(id, ct);
return Results.Ok(user);
});
Best practices
- Use async all the way down, including data access and HTTP calls.
- Accept and propagate CancellationToken to downstream services.
- Return
Taskor ValueTask from async APIs. - Apply timeouts to external calls and long-running operations.
Common pitfalls
- Sync-over-async (.Result, .Wait()), which blocks threads.
- Mixing blocking and async code in the same request path.
- Ignoring cancellation tokens and continuing work after the request is aborted.
Technique 3: Database Optimization
Your API is only as fast as its slowest query. Most “API performance” issues are actually database problems in disguise.
Optimize queries and projections
var products = await dbContext.Products
.Where(p => p.IsActive)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.ToListAsync();
Best practices
- Always project to DTOs.
- Use proper indexes.
- Analyze execution plans regularly.
Common pitfalls
- Returning full entities.
- N+1 query patterns.
- Missing indexes on filtered columns.
Technique 4: Efficient JSON Serialization
Serialization overhead becomes visible as traffic increases. Choosing the right serializer and configuration matters.
Prefer System.Text.Json for performance
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.SerializerOptions.DefaultIgnoreCondition =
JsonIgnoreCondition.WhenWritingNull;
});
Best practices
- Use System.Text.Json unless you need advanced features.
- Avoid deep object graphs.
- Benchmark real payloads.
Common pitfalls
- Serializing unused fields.
- Custom converters without profiling.
- Large nested responses.
Technique 5: Response Compression
Reducing payload size improves perceived performance, especially on mobile networks.
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
});
app.UseResponseCompression();
Best practices
- Enable compression globally.
- Combine with payload minimization.
- Prefer Brotli when possible.
Common pitfalls
- Compressing already compressed data.
- Ignoring CPU impact under load.
Technique 6: Minimizing Payload Size
Sending less data is often more effective than optimizing how fast you send it.
Best practices
- Design purpose-built DTOs.
- Avoid exposing domain models.
- Remove unused fields aggressively.
Common pitfalls
- Ignoring frontend usage patterns.
- “Just return everything.”
- Versioning APIs by adding fields only.
Technique 7: Connection Pooling and Resource Management
Improper resource handling reduces throughput even if individual requests look fast.
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
// execute commands
Best practices
- Dispose connections properly.
- Keep connection lifetimes short.
- Trust ADO.NET pooling.
Common pitfalls
- Long-lived connections.
- Manual pooling implementations.
- Forgetting disposal in error paths.
Technique 8: HTTP/2 and Protocol Improvements
Modern protocols reduce latency without touching your code.
Best practices
- Enable HTTP/2 in Kestrel.
- Verify hosting environment support.
- Combine with TLS and compression.
Common pitfalls
- Assuming HTTP/2 is always enabled.
- Not testing under real traffic.
Technique 9: Middleware Pipeline Optimization
Every middleware runs on every request. Order and necessity matter.
app.UseResponseCompression();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
Best practices
- Keep the pipeline short.
- Order middleware intentionally.
- Apply conditional middleware when possible.
Common pitfalls
- Over-logging.
- Global exception handling everywhere.
- Redundant middleware.
Technique 10: Rate Limiting and Throttling
Protect your API from abuse and traffic spikes.
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("default", limiter =>
{
limiter.Window = TimeSpan.FromMinutes(1);
limiter.PermitLimit = 100;
});
});
app.UseRateLimiter();
Best practices
- Start simple.
- Tune limits with real traffic.
- Apply per-endpoint rules.
Common pitfalls
- Overly aggressive limits.
- No client feedback.
- Global limits for all endpoints.
Technique 11: Profiling and Monitoring
If you don’t measure, you guess.
Best practices
- Use Application Insights or OpenTelemetry.
- Track latency percentiles, not averages.
- Profile before optimizing.
Common pitfalls
- Logging without metrics.
- Optimizing without data.
- Ignoring production behavior.
What Changes with .NET 10
Upgrading to .NET 10 does not magically fix slow APIs, but it does reward good design. Most improvements come from internal optimizations in ASP.NET Core, Kestrel, async execution, and serialization, which make well-structured APIs slightly faster without code changes. Minimal APIs are more mature, with better tooling and diagnostics, but they still require discipline to avoid logic-heavy endpoints. Built-in features like rate limiting feel more complete and reduce the need for external libraries. Serialization keeps improving, yet oversized payloads remain a bigger problem than serializer speed. Native AOT is more viable, but still niche for typical business APIs. In the end, .NET 10 reinforces a simple rule: clean architecture scales better, and the runtime amplifies your decisions rather than replacing them.
Before moving to the practical example, one important aspect deserves a mention: error handling.
Many APIs return inconsistent or ad-hoc error responses, making clients harder to implement and maintain.
ASP.NET Core provides a standardized way to represent errors using ProblemDetails, which helps create predictable and self-describing APIs.Consistent error responses are not just a design concern, but also a resilience and maintainability feature.
Practical Example: Building a Minimal API with a Service Layer (No Database)
Disclaimer: This example is purely for educational purposes. There are better ways to write code and applications that can optimize this example. Use this as a starting point for learning, but always strive to follow best practices and improve your implementation.
This small example demonstrates several production-oriented decisions:
- asynchronous execution with cancellation support
- explicit service boundaries
- stable API contracts via DTOs
- consistent error handling using ProblemDetails
All of this without introducing a database or unnecessary infrastructure.
Prerequisites
Before starting, make sure you have the following installed:
- .NET SDK: Download and install the .NET SDK if you haven’t already.
- Visual Studio Code (VSCode): Install Visual Studio Code for a lightweight code editor.
- C# Extension for VSCode: Install the C# extension for VSCode to enable C# support.
Step 1 – Create an API Project
Create a new API project:
dotnet new web -n UsersApi
cd UsersApi
Open the project in Visual Studio Code:
code .
Step 2 – Define the Data Contract
Instead of exposing internal models, we define a simple DTO that represents the API response.
public sealed record UserDto(int Id, string Name);
Using DTOs keeps the API contract explicit and stable.
Step 3 – Start with a Simple Service
Before writing any API code, we define a small service that represents our application logic.
For simplicity, this example uses an in-memory store instead of a database.
Create a UserService class.
public sealed class UserService
{
private static readonly IReadOnlyDictionary<int, UserDto> Users =
new Dictionary<int, UserDto>
{
[1] = new UserDto(1, "Alice"),
[2] = new UserDto(2, "Bob"),
[3] = new UserDto(3, "Charlie")
};
public async Task<UserDto?> GetUserAsync(int id, CancellationToken ct)
{
// Simulate I/O latency
await Task.Delay(50, ct);
Users.TryGetValue(id, out var user);
return user;
}
}
This service already supports asynchronous execution and cancellation, which makes it suitable for real API scenarios.

Step 4 – Expose the API Endpoint
Now we expose the service through a Minimal API endpoint.
Open Program.cs and add the following code:
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddSingleton<UserService>();
var app = builder.Build();
app.MapGet("/users/{id}", async (
int id,
UserService service,
CancellationToken ct) =>
{
if (id <= 0)
{
return Results.Problem(
title: "Invalid user id",
detail: "The user id must be a positive integer.",
statusCode: StatusCodes.Status400BadRequest);
}
var user = await service.GetUserAsync(id, ct);
if (user is null)
{
return Results.Problem(
title: "User not found",
detail: $"No user with id {id} exists.",
statusCode: StatusCodes.Status404NotFound);
}
return Results.Ok(user);
});
app.Run();

Step 5 – Run and Test the API
Start the application:
dotnet run
Open a browser or an HTTP client and test the endpoint:
GET /users/1
Try also invalid and non-existing IDs to see how consistent error responses are returned.

Here the full code:
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddSingleton<UserService>();
var app = builder.Build();
app.MapGet("/users/{id}", async(
int id,
UserService service,
CancellationToken ct) =>
{
if (id <= 0)
{
return Results.Problem(
title: "Invalid user id",
detail: "The user id must be a positive integer.",
statusCode: StatusCodes.Status400BadRequest
);
}
var user = await service.GetUserAsync(id, ct);
if (user is null)
{
return Results.Problem(
title: "User not found",
detail: $"No user with id {id} exists.",
statusCode: StatusCodes.Status404NotFound
);
}
return Results.Ok(user);
});
app.Run();
public sealed record UserDto(int Id, string Name);
public sealed class UserService
{
private static readonly IReadOnlyDictionary<int, UserDto> Users =
new Dictionary<int, UserDto>
{
[1] = new UserDto(1, "Alice"),
[2] = new UserDto(2, "Otto"),
[3] = new UserDto(3, "Charlie"),
};
public async Task<UserDto?> GetUserAsync(int id, CancellationToken ct)
{
// Simulate I/O latency
await Task.Delay(50, ct);
Users.TryGetValue(id, out var user);
return user;
}
}
Conclusion
Improving ASP.NET Core Web API performance and resilience is not about applying a single optimization or following a checklist. It is the result of many small, intentional decisions made over time: how data is accessed, how payloads are shaped, how errors are handled and how resources are managed under load.
Modern .NET versions provide excellent building blocks, but they do not replace good architecture. Minimal APIs make it easier to ship quickly, yet they require discipline to remain maintainable and scalable. Techniques like caching, async programming, cancellation support and consistent error handling work best when combined, not applied in isolation.
The goal is not to build the perfect API from day one, but to build APIs that behave predictably, scale responsibly, and can evolve as real-world constraints emerge. Measure, iterate, and refine and let the runtime amplify good decisions instead of exposing bad ones.
If you think your friends or network would find this article useful, please consider sharing it with them. Your support is greatly appreciated.
Thanks for reading!
Discover CodeSwissKnife Bar, your all-in-one, offline Developer Tools from Your Menu Bar
