1. Make Everything Asynchronous
Why? Blocking threads on I/O operations (like database, HTTP, or file calls) wastes server resources and limits scalability. Asynchronous programming allows your API to serve more requests simultaneously.
How?
- Always use
async/awaitfor I/O-bound operations. - Make your entire call chain async—from controller to repository.
Example:
[HttpGet("products")]
public async Task<IActionResult> GetProductsAsync() {
var products = await _repository.GetAllAsync();
return Ok(products);
}
Tip: Avoid mixing synchronous and asynchronous code (e.g., don’t use .Result or .Wait() in async code paths).
2. Optimize Data Access
Data access is often the main bottleneck in Web APIs. Use these strategies:
a) Use AsNoTracking for Read-Only Queries
Why? Entity Framework Core’s change tracking adds overhead. If you don’t need to update entities, disable tracking.
Example:
var products = await _context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync();
b) Project Only What You Need
Why? Fetching only the required columns reduces network and memory usage.
Example:
var productDtos = await _context.Products
.AsNoTracking()
.Select(p => new ProductDto {
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.ToListAsync();
c) Use Dapper for Simple, High-Performance Queries
For extremely high-throughput endpoints or reports, Dapper is a great alternative.
Example:
var sql = "SELECT Id, Name, Price FROM Products WHERE IsActive = 1";
var products = await connection.QueryAsync<ProductDto>(sql);
d) Add Database Indexes
Profile your queries and ensure your most common filters and sorts have appropriate indexes.
3. Reduce Payload Size (Filter, Paginate, Shape Responses)
APIs shouldn’t send unnecessary data.
Why? Large payloads slow down both the client and the server (more memory, more network bandwidth).
How?
- Pagination: Return results in pages.
- Filtering: Allow clients to specify criteria.
- Shaping: Only return fields the client needs (DTOs).
Example:
[HttpGet]
public async Task<IActionResult> GetProducts([FromQuery]int page = 1, [FromQuery]int pageSize = 20) {
var products = await _context.Products
.AsNoTracking()
.OrderBy(p => p.Name)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(p => new ProductListItemDto {
Id = p.Id,
Name = p.Name
})
.ToListAsync();
return Ok(products);
}
4. Minimize Serialization Overhead
Serialization can become a bottleneck, especially for large responses.
How?
- Use
System.Text.Jsoninstead ofNewtonsoft.Jsonfor better performance (it’s the default in modern .NET). - Configure serialization options to skip nulls, use camelCase, and ignore unnecessary properties.
Example:
builder.Services
.AddControllers()
.AddJsonOptions(opts => {
opts.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
opts.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});
Advanced: For very large objects or high-traffic endpoints, consider compressing JSON further or switching to more compact formats like MessagePack.
5. Leverage Caching
Caching is often the single most effective way to speed up Web APIs, especially for data that doesn’t change with every request.
a) In-Memory Cache
services.AddMemoryCache();
var product = await _cache.GetOrCreateAsync($"product_{id}", async entry => {
entry.SlidingExpiration = TimeSpan.FromMinutes(5);
return await _context.Products.FindAsync(id);
});
b) Distributed Cache (Redis, SQL Server)
Use this for load-balanced or cloud environments.
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
});
c) Response Caching
Cache the entire HTTP response for certain endpoints.
[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any)]
[HttpGet("products")]
public IActionResult GetCachedProducts() {
// Return products
}
d) OutputCache (ASP.NET Core 7+)
For flexible caching based on routes, headers, or custom policies.
6. Enable Response Compression
Reducing the size of HTTP responses can dramatically improve client-perceived speed, especially over slow networks.
How?
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
});
app.UseResponseCompression();
This enables Brotli or Gzip compression by default.
7. Optimize Your Middleware Pipeline
The order of middlewares matters!
- Place response compression and static files at the top.
- Place authentication/authorization before routing.
- Custom middlewares (logging, error handling) should be early to catch more issues.
Example:
app.UseResponseCompression();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
});
8. Use HTTP/2 or HTTP/3
Why? HTTP/2 and HTTP/3 protocols support multiplexing, header compression, and better connection reuse—leading to faster API calls.
How?
Kestrel supports both. In appsettings.json:
"Kestrel": {
"Endpoints": {
"Https": {
"Protocols": "Http1AndHttp2"
}
}
}
Or for HTTP/3 (from .NET 7+):
"Kestrel": {
"Endpoints": {
"Https": {
"Protocols": "Http1AndHttp2AndHttp3"
}
}
}
9. Serve Static Assets via CDN
If your API serves images, JS, or other assets, move them to a CDN. This reduces the load on your API and ensures fast global delivery.
Example:
- Use Azure Blob Storage with CDN, Amazon S3 + CloudFront, or a third-party like Cloudflare.
10. Implement Rate Limiting and Throttling
Why? To protect your API from abuse, ensure fair usage, and prevent spikes from taking your service down.
How? Use libraries like AspNetCoreRateLimit:
services.AddInMemoryRateLimiting();
app.UseIpRateLimiting();
Configure rules in appsettings.json.
11. Switch to Minimal APIs (Where Appropriate)
Minimal APIs offer:
- Lower overhead (no controller/middleware bloat)
- Direct route-to-code mapping
- Measurably higher throughput and lower memory use (especially on .NET 8/9)
Example:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/ping", () => "pong");
app.MapGet("/products/{id}", async (int id, MyDbContext db) =>
await db.Products.FindAsync(id)
);
app.Run();
When to Use?
- Microservices
- Lightweight internal APIs
- Endpoints that don’t need the full MVC feature set
12. Expose GraphQL Endpoints for Flexible Data Queries
GraphQL allows clients to request exactly the data they need, reducing over-fetching and under-fetching.
How?
- Use libraries like HotChocolate.
Example:
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>();
public class Query {
public IQueryable<Product> GetProducts([Service] MyDbContext db)
=> db.Products;
}
13. Profile and Benchmark Your API
Never optimize blindly! Use:
- BenchmarkDotNet
- dotTrace
- Application Insights
- k6 for load testing
Tip: Measure before and after every change to ensure you’re improving what actually matters.
14. Miscellaneous Quick Wins
- Avoid “N+1” queries (use
.Include()for related data in EF Core) - Return 204 NoContent when appropriate instead of empty lists
- Reuse HttpClient instances (don’t create per request)
- Set connection pooling for databases
- Profile with tools like MiniProfiler or EF Core logging
Conclusion
Optimizing .NET Web API performance isn’t about a single trick—it’s about applying best practices at every layer: async I/O, data access, payload shaping, caching, and transport. Measure, tweak, and repeat. By following these strategies, your APIs will be faster, more scalable, and more enjoyable for your users.