Throughput, memory and cold start directly drive hosting costs and user-perceived latency.
ASP.NET Core is among the fastest mainstream frameworks, but app code usually dominates. The main levers:
- Never block on async code (
.Result,.Wait(), synchronous I/O). Blocking starves the thread pool and causes latency spikes under load ("thread pool starvation"). Synchronous body reads are disallowed by default for this reason. - Use IHttpClientFactory (
AddHttpClient<T>) for outbound calls: pooled handlers, DNS refresh, and resilience viaMicrosoft.Extensions.Http.Resilience(AddStandardResilienceHandler). - Stream large responses (
IAsyncEnumerable<T>is serialized as it's produced) instead of buffering whole lists. - Reduce allocations in hot paths; check with
dotnet-counters(allocation rate, GC, thread pool queue length) anddotnet-trace. - Paginate and project database queries; most "slow API" problems are queries.
Native AOT compiles the app to a native executable: startup in tens of milliseconds and much lower memory, great for serverless and dense container hosting. It's supported for minimal APIs, gRPC and worker services, not MVC or Razor. Start with dotnet new webapiaot or set <PublishAot>true</PublishAot>, use WebApplication.CreateSlimBuilder (fewer default features), and register a JSON source-generated context so serialization needs no reflection. The Request Delegate Generator turns your Map* handlers into compile-time code automatically when AOT is enabled.
Fix every AOT/trim warning: they predict runtime failures.
Example
// <PublishAot>true</PublishAot> in the csproj; dotnet publish -r linux-x64
using System.Text.Json.Serialization;
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.ConfigureHttpJsonOptions(o =>
o.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJson.Default));
var app = builder.Build();
app.MapGet("/todos", () => Stream(1000)); // streamed JSON array
app.MapGet("/todos/{id:int}", (int id) => new Todo(id, $"Todo {id}", false));
app.Run();
static async IAsyncEnumerable<Todo> Stream(int n)
{
for (int i = 1; i <= n; i++)
{
if (i % 100 == 0) await Task.Yield(); // e.g. awaiting DB pages
yield return new Todo(i, $"Todo {i}", i % 2 == 0);
}
}
public record Todo(int Id, string Title, bool Done);
[JsonSerializable(typeof(Todo))]
[JsonSerializable(typeof(IAsyncEnumerable<Todo>))]
internal partial class AppJson : JsonSerializerContext { }Remember
- Blocking on async code causes thread pool starvation under load
- Use IHttpClientFactory and stream large responses
- Native AOT works with minimal APIs: CreateSlimBuilder + JSON source generation
- Treat trim and AOT warnings as errors
Watch out for
- Calling .Result on HttpClient calls inside request handlers, which looks fine in tests and collapses under load.