Lessons Learned – ASP.NET Core 6 Performance

I had just written an API using ASP.NET Core and it looked pretty good.  My solution was using clean architecture – separating out Core logic from Infrastructure and so on.  My project was also using CQRS, MediatR, FluentValidation.  Everything was organized and fairly easy to understand.

However, the moment of truth came when I ran some load tests using K6 –  although the code was set up great, I definitely needed to optimize for performance.   I needed this API to perform well under heavy load.

Here are the top areas that made the most impact for me so far

  • Use Correct Dependency Injection Service Lifetimes
    • I previously registered dependencies for this API mostly using the “Transient” lifetime.  This means that every time a dependency needed to be resolved, it would always create a new instance. 
    • When doing web API calls, the “Scoped” lifetime is probably going to be best.  The instances can be shared for the scope of the HTTP request.  When your API is under heavy load, this could make a difference in the number of objects created in memory which may impact performance.  In my case, making this change in service registration did make a big difference.
  • Single trip to the database for each HTTP request (Not specific to ASP.NET Core)
    • This is most important in your “hot paths” – the requests that will be made the most frequently.
    • In my fluent validation classes, I was making calls to the database to validate a request before doing what I needed to do.  I wanted all my validation code to be together in one place, but this did result in multiple trips to the database for a request.  If you are trying to optimize for a heavy load, these extra trips to the database do affect performance.  Although, it was prettier to have it all in my fluent validation class, I had to move this SQL validation checks to be moved into the stored procedure that did other SQL work for that request so there would only be 1 trip to the database.  It wasn’t the prettiest, but that can happen when you need to optimize performance.
  • Database schema tweaks (not specific to ASP.NET Core)
    • Previously I was using a single table to manage records which had a “status” column.  This table was being read, inserted into, and updated, and deleted to manage the records’ status.
      • Under heavy load, having all these CRUD operations done could be blocking each other – especially the updates and deletes.
        • I moved the status column to a separate StatusLog table to be only inserted into along with a timestamp.  This is where I would now find the latest status.
        • I created a SQL Job to be run on a time interval to delete records only if the SQL Server’s instance had 90% free CPU
    • Adding indexes where needed.
      • Check your queries/look at query execution plans to see what indexes may be missing
      • While running a load test, you can also use the below stored procedures to analyze what was going on.
  • Cache JWT Tokens for validation
    • Another area that can be optimized is the token validation.  Whenever a client is making a request to your API, it needs to include a bearer token for authorization.  This bearer token needs to be validated by your API.  Rather than having to re-validate the token every time, you can cache the token’s result in-memory (or a distributed cache) and save time.  Here is a video I found on YouTube that helped guide me in how to do this – Improving JWT performance in ASP.NET Core webinar with Mentor – Marcin Hoppe.  This video also shows the performance improvement in load tests as well.

More Tips for Performance

I hope this article on my experience with improving ASP.NET Core performance was helpful!  There’s a lot more that can be done in addition to what I’ve mentioned so far.  You can find a good article on Microsoft’s documentation here – ASP.NET Core Best Practices | Microsoft Learn