Thank you to our sponsors who keep this newsletter free to the reader:
This week's issue is sponsored by QuadSpinner.
Performance optimization is my favorite thing about software engineering. Over the last 5 years, I've encountered various performance problems that taught me different ways to overcome them.
About a month ago, I ran into an issue with an API endpoint that wasn't scaling well.
This endpoint is used to calculate a report for an e-commerce web application. It needed to talk to multiple modules (services) to gather all the necessary data, combine it and perform the calculations.
I made a post about it on LinkedIn that resonated with many people.
In this newsletter, I want to break down what I did to achieve a 15x performance improvement.
The first thing I do when I'm solving a performance problem is determine where the slowest piece of the code is. Fixing this part of the code will usually give the most significant improvement.
Solving one bottleneck can also reveal where the next bottleneck is.
This is a continual process.
In my situation, there were a few bottlenecks:
- Calling the database from a loop
- Calling an external service multiple times
- Executing a complex calculation multiple times with identical parameters
How can you measure performance?
A simple approach can be using
System.Timers.Timer where you
manually log execution times between method calls.
Or you can use a performance profiler.
A round trip between your application and a database (or some other service) can last 5-10ms, or more. If you have many round trips in your flow, it's going to add up quickly.
Here are a few things you can do reduce the number of round trips:
- Don't call the database from a loop. This can usually be solved with a simple query like this:
SELECT * FROM [TableName] WHERE Id IN (list_of_ids)
Use a query that returns multiple result sets from the database. One library that supports this is Dapper, with the
If you need to make multiple calls to another service, try to convert that into one call. And in the service, aggregate the required data and return everything at once.
I had a situation where I was awaiting multiple asynchronous calls from a few services. These calls had no dependencies on each other, so I used a simple technique to gain a significant performance improvement.
Let's say you're awaiting two tasks:
var task1Result = await CallService1Async();
var task2Result = await CallService2Async();
// Use the results.
A simple way to parallelize these calls is using the
var task1 = CallService1Async();
var task2 = CallService2Async();
await Task.WhenAll(task1, task2);
// Use the results.
Notice that I'm directly accessing the
Result property on the tasks.
This can be detrimental if you're using it to block on an asynchronous call,
and can even lead to deadlocks.
However, in this situation it is perfectly safe to do,
because the two tasks will have completed after the call to
Of course, whether or not these tasks will be executed in parallel
Task.WhenAll depends on a few factors, which I won't cover here.
I try to leave caching for the end, after I have exhausted all other possibilities to improve performance. While I love to use caching in general, I'm aware it can introduce some unwanted behavior when data is stale.
You have to consider how long you can safely cache the data, and how you are going to clear the cache if the underlying data changes.
In simple applications, I use
that is available in ASP.NET Core out of the box.
But you can also use an external cache like Redis.
A good candidate for caching is data that is frequently accessed, but rarely modified.
I think that for most Web applications, performance optimization can be boiled down to the following approaches:
I didn't talk about database optimization and indexes here, but this should also be on your mind if the database is your bottleneck.