How to Increase the Performance of Web APIs in .NET
A friendly, step-by-step guide to making your ASP.NET Core Web APIs fast: async, caching, query tuning, compression, and pooling in .NET 10.
A kitchen at lunchtime
Imagine a small restaurant at lunchtime. Orders are pouring in. If the cook makes one dish, walks it to the table, comes back, and only then starts the next order, the queue grows huge and customers leave angry.
A smart kitchen works differently. The cook starts the rice boiling and, while it cooks, chops vegetables for another order. Popular dishes are kept ready in a warm tray, so they go out the moment someone asks. The waiter carries several plates in one trip instead of one plate per walk.
A Web API is exactly like that kitchen. Requests are the orders. The database is the slow stove. Your job is to keep the kitchen busy and never let one slow task block everything else. In this guide we will learn the simple, proven ways to make a .NET Web API fast — using clear pictures, small code, and plain English.
We will use .NET 10, which is the current Long-Term Support (LTS) release, and C# 14.
First rule: measure before you change anything
Before you speed up a kitchen, you watch it and find the slowest spot. Maybe the stove is fine but the waiter is slow. Code is the same. Never guess. Measure, find the slowest step, fix it, then measure again.
The optimization loop
Steps
Measure
Add timing, logs, metrics
Find slowest step
Database? Serialization? Network?
Fix one thing
Change only that part
Measure again
Prove the change helped
A good API request usually breaks down into a few stages. Knowing where time goes tells you what to fix.
In most real APIs, the database / external call stage (D) is the slowest by far. That is why so many of our tips below are about reducing or avoiding that work. Keep this picture in your head as you read.
Tip 1: Use async all the way down
This is the most important habit. When your code waits on the database or the network, it should not hold a thread hostage. Async lets a small pool of threads serve thousands of requests, because waiting threads are freed to do other work.
Think of it like the cook starting the rice and walking away to chop vegetables, instead of standing and staring at the pot.
The rule is simple: if a method has an async version, use it, and await it. Never call .Result or .Wait() on a task — that blocks the thread and can deadlock your app.
// Slow: blocks a thread the whole time
app.MapGet("/products", (AppDbContext db) =>
{
var items = db.Products.ToList(); // blocking
return Results.Ok(items);
});
// Fast under load: thread is freed while waiting
app.MapGet("/products", async (AppDbContext db) =>
{
var items = await db.Products.ToListAsync(); // async
return Results.Ok(items);
});Async does not make a single request finish faster. It lets your server handle many more requests at once without running out of threads. Under real traffic, that is the difference between a smooth API and one that falls over.
Tip 2: Cache work you keep repeating
The biggest single win for most APIs is caching. If you save the result of slow work and return the saved copy next time, you skip the slow part entirely. It is the warm tray of ready dishes in our kitchen.
In .NET 10, the recommended modern tool is HybridCache. It combines a fast in-memory layer (L1) with an optional shared store like Redis (L2), and it protects you from a "stampede" — where a popular item expires and hundreds of requests all hit the database at once. With HybridCache, only one request does the slow work; the rest wait for that single result.
// Program.cs
builder.Services.AddHybridCache();
// In an endpoint or service
public async Task<Product> GetProductAsync(int id, HybridCache cache)
{
return await cache.GetOrCreateAsync(
$"product-{id}", // cache key
async token => await LoadFromDbAsync(id, token),
cancellationToken: default);
}The first call is slow (it misses and hits the database). Every call after that is fast until the cached copy expires. For data read far more often than it changes — product details, settings, lookup lists — this is a huge speed-up. Microsoft notes HybridCache can cut database hits by 50 to 90 percent in high-read scenarios.
Here is a quick guide to the caching choices:
| Cache type | Where it lives | Best for | Shared across servers? |
|---|---|---|---|
IMemoryCache | One server's RAM | Simple single-server apps | No |
| Distributed (Redis) | Separate store | Many servers behind a load balancer | Yes |
HybridCache | RAM + optional Redis | New apps, best of both | Yes (with L2) |
| Output cache | Whole HTTP response | Same response for many users | Yes (configurable) |
Tip 3: Fix slow database queries
Even with caching, you will hit the database. So make those trips count. The most common mistake is the N+1 query problem: you fetch a list of N items, then loop and run one more query per item. One hundred orders becomes one hundred and one round trips to the database.
N+1 problem vs one good query
Steps
Get list
1 query for N orders
Loop each
For every order...
Query per item
N more queries (slow!)
One joined query
Use Include — 1 trip
The fix in Entity Framework Core is to load related data together with Include, and to fetch only the columns you actually need.
// Slow: N+1 — one query per order's customer
var orders = await db.Orders.ToListAsync();
foreach (var o in orders)
{
var name = o.Customer.Name; // triggers a query each time
}
// Fast: load related data in one query
var orders = await db.Orders
.Include(o => o.Customer)
.Where(o => o.Status == "Open") // filter in the database
.Select(o => new OrderDto( // fetch only needed columns
o.Id, o.Customer.Name, o.Total))
.AsNoTracking() // read-only, skip change tracking
.ToListAsync();Three habits to remember for read-only queries:
- Filter in the database, not in C# memory. Push your
Whereto the database so it returns only the rows you need. - Select only the columns you use with a small DTO. Do not drag back huge entities you will not read.
- Use
AsNoTracking()for read-only data. It skips EF Core's change-tracking bookkeeping and is faster and lighter.
These three changes often turn a sluggish endpoint into a fast one without any caching at all.
Tip 4: Send smaller responses
A response that is half the size arrives in half the time over a slow network. There are two easy ways to shrink responses.
Compression. Turn on response compression so JSON is squeezed with Brotli (best) or Gzip before it leaves your server. For typical JSON, this cuts size by 60 to 80 percent.
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
});
var app = builder.Build();
app.UseResponseCompression();Send less data. Do not return giant lists in one response. Use paging so a request for GET /products returns 20 items, not 20,000. A paged endpoint takes a page number and size, like GET /products?page=2&size=20, and returns only that slice.
| Technique | What it does | Typical saving |
|---|---|---|
| Brotli / Gzip compression | Shrinks the bytes on the wire | 60–80% smaller JSON |
| Paging | Returns a slice, not everything | Huge on big lists |
| Slim DTOs | Returns only needed fields | Smaller + clearer |
| Source-generated JSON | Faster serialize, less allocation | Lower CPU + AOT-ready |
For JSON serialization, .NET's System.Text.Json source generator builds the serializer at compile time. That means faster serialization, fewer memory allocations, and it works with Native AOT.
[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(List<Product>))]
public partial class ApiJsonContext : JsonSerializerContext
{
}
// Register it
builder.Services.ConfigureHttpJsonOptions(o =>
o.SerializerOptions.TypeInfoResolverChain.Insert(0, ApiJsonContext.Default));Tip 5: Reuse connections — never create them per request
Creating a new HttpClient for every call is like buying a new phone every time you make a call. It is wasteful and can exhaust the operating system's connections (a problem called socket exhaustion). Instead, use IHttpClientFactory, which pools and reuses connections for you.
// Program.cs
builder.Services.AddHttpClient("catalog", c =>
{
c.BaseAddress = new Uri("https://catalog.example.com");
});
// Use it
app.MapGet("/external", async (IHttpClientFactory factory) =>
{
var client = factory.CreateClient("catalog");
var data = await client.GetStringAsync("/items");
return Results.Ok(data);
});The same idea applies to database connections — EF Core and ADO.NET already pool them for you, so let the pool do its job and do not open connections by hand.
Tip 6: Tune the runtime — turn on Server GC
The .NET garbage collector cleans up memory you no longer use. For a busy server, Server GC is the right mode. It uses multiple threads and is built for throughput, so your API can churn through many requests with shorter pauses. Most server templates enable it already, but it is worth checking your project file.
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
</PropertyGroup>If you publish with Native AOT, your API starts much faster and uses less memory — great for serverless and containers. Just remember AOT needs source-generated JSON (Tip 4), because it cannot use runtime reflection.
Putting it together: the request journey
Here is how a well-tuned API handles a request. Notice how many slow steps it skips.
A request that hits the cache never touches the database at all. A request that misses does the work once, saves it, and makes every future request fast. On the way out, the response is compressed so it reaches the user quickly. That is the whole game: do slow work as rarely as possible, and send as few bytes as you can.
A note on libraries and licensing
Some popular .NET libraries that you might reach for in API projects have changed their licensing. MediatR and MassTransit are now commercially licensed for many uses. They are fine tools, but you do not need them to build a fast API — plain ASP.NET Core minimal APIs, EF Core, and HybridCache cover everything in this guide for free. Choose libraries with open eyes, and check the license before you depend on one.
Common mistakes that quietly slow you down
- Blocking on async with
.Resultor.Wait(). This wastes threads and risks deadlocks. Alwaysawait. - Returning whole entities with dozens of columns when the client needs three fields. Use a DTO.
- Logging too much in hot paths. Writing a log line on every loop iteration adds up fast. Log what matters.
- No paging on list endpoints. A
GET /itemsthat returns everything will eventually return too much. - Caching data that changes constantly. Caching only helps when data is read far more often than it changes.
- Optimizing without measuring. You will spend hours speeding up code that was never the problem.
Quick recap
- Measure first. Find the slowest step, fix that one thing, then measure again.
- Go async all the way. Use
awaitand the...Asyncmethods so your server handles many requests at once. - Cache repeated work. Use
HybridCachein .NET 10 to skip the database for read-heavy data. - Fix slow queries. Avoid the N+1 problem; use
Include, filter in the database, select only needed columns, andAsNoTracking()for reads. - Send smaller responses. Turn on Brotli/Gzip compression, page your lists, and use slim DTOs.
- Reuse connections. Use
IHttpClientFactory; let EF Core pool database connections. - Tune the runtime. Enable Server GC; consider Native AOT for fast cold starts.
- Mind licensing. MediatR and MassTransit are now commercially licensed — you do not need them for a fast API.
References and further reading
- ASP.NET Core Best Practices — Microsoft Learn
- HybridCache library in ASP.NET Core — Microsoft Learn
- Response compression in ASP.NET Core — Microsoft Learn
- Overview of caching in ASP.NET Core — Microsoft Learn
- ASP.NET Core performance overview — Microsoft Learn
Related Posts
Caching in ASP.NET Core: Make Your App Fast (The Easy Way)
Learn caching in ASP.NET Core with simple examples. Understand in-memory cache, distributed Redis cache, HybridCache, and output cache, with diagrams, code, and clear advice on which to use and when.
Building Async APIs in ASP.NET Core the Right Way
Learn to build fast, safe async APIs in ASP.NET Core: async/await, CancellationToken, avoiding .Result deadlocks, and thread pool tips.
ASP.NET Core Output Cache: Speed Up Your API with In-Memory and Redis
Learn ASP.NET Core output caching the easy way: cache whole API responses in memory or in Redis, set policies, vary by query, and clear with tags — with diagrams and code.
Top 15 Mistakes Developers Make When Creating Web APIs
A warm, beginner-friendly tour of the 15 most common Web API mistakes in ASP.NET Core, with simple fixes, diagrams, tables, and clear C# examples.
Problem Details for ASP.NET Core APIs: A Beginner's Guide
Learn Problem Details in ASP.NET Core step by step. Give your API one clean error format with RFC 9457, AddProblemDetails, and IExceptionHandler in .NET 10.
How to Implement Caching Strategies in .NET: A Beginner-Friendly Guide
Learn caching strategies in .NET with simple examples: in-memory cache, distributed Redis cache, and HybridCache, plus eviction, stampede protection, and tags.