Back to Blog

.NET performance tips

Concrete practices to improve throughput and reduce latency in modern .NET applications — from object pooling to correct async usage.

2 min read
.NETC#performance

Performance in .NET isn't magic: it's about measuring, understanding the bottleneck and applying the right optimization. Here are some tips I use in production.

1. Prefer ArrayPool and ObjectPool for hot allocations

In hot paths, avoiding allocations is more important than micro-optimizing algorithms. ArrayPool<T> reuses buffers and reduces GC pressure.

using System.Buffers;
 
public byte[] ProcessData(ReadOnlySpan<byte> input)
{
    byte[] buffer = ArrayPool<byte>.Shared.Rent(input.Length);
    try
    {
        // ...process using buffer...
        input.CopyTo(buffer);
        return buffer[..input.Length].ToArray();
    }
    finally
    {
        ArrayPool<byte>.Shared.Return(buffer);
    }
}

2. Use async correctly

async isn't a synonym for "fast". There is overhead to a state machine. For truly synchronous and cheap operations, avoid async.

public async Task<User> GetUserAsync(int id)
{
    await using var ctx = new AppDbContext();
    return await ctx.Users
        .AsNoTracking()
        .FirstOrDefaultAsync(u => u.Id == id)
        .ConfigureAwait(false);
}

Use ConfigureAwait(false) in library code to avoid capturing the synchronization context.

3. Avoid LINQ in hot loops

LINQ is beautiful, but it creates closures and extra iterations. In high-frequency loops, prefer for:

int sum = 0;
for (int i = 0; i < items.Length; i++)
{
    sum += items[i].Value;
}

4. Measure before optimizing

Use dotnet-counters, dotnet-trace and BenchmarkDotNet before changing anything. Optimizing without measuring is putting out an imaginary fire.

dotnet trace collect --providers Microsoft-DotNETCore-SampleProfiler

Conclusion

Performance is cumulative: every allocation avoided, every ConfigureAwait(false), every loop converted from LINQ to for contributes to a healthier application in production.