How I Optimized an API Endpoint to Make It 15x Faster

How I Optimized an API Endpoint to Make It 15x Faster

4 min read ·

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.

Focus On Bottlenecks First

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.

Reduce The Number of Round Trips

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:

  1. 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)
  1. Use a query that returns multiple result sets from the database. One library that supports this is Dapper, with the QueryMultiple method.

  2. 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.

Parallelize External Calls

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 Task.WhenAll method:

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 Task.WhenAll completes.

Of course, whether or not these tasks will be executed in parallel when calling Task.WhenAll depends on a few factors, which I won't cover here.

Caching As a Last Resort

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 IMemoryCache 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.

Closing Thoughts

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.

Whenever you're ready, there are 4 ways I can help you:

  1. Modular Monolith Architecture (NEW): Join 600+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  2. Pragmatic Clean Architecture: Join 2,750+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  3. Patreon Community: Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.
  4. Promote yourself to 51,000+ subscribers by sponsoring this newsletter.

Become a Better .NET Software Engineer

Join 51,000+ engineers who are improving their skills every Saturday morning.