Hjelle.Dev

Performance boosting with SemaphoreSlim

Performance is crucial for anyone with a website or an Api. Slow pages or endpoints can lead to the loss of users and revenue. Because who wants to use a slow service? I know that I don’t want to wait on my data.

The first step is to identify the right places to remedy. This is not straight forward. But the more data you have, the better. Some parts of a website are more important than others. I would put my focus on the most used sites and calls. I would also think about the lesser used sites and calls. Don’t people use them because they are slow? Or are they just not that important in daily use?

Get NordVPN - the best VPN service for a safer and faster internet experience. Get up to 77% off + 3 extra months with a 2-year plan! Take your online security to the next level

Get NordVPN - Secure Your Connection

This is an affiliate link. I may earn a commission when you click on it and make a purchase at no extra cost to you.

There are several good tools for making these decisions:

-              Google analytics

-              Azure Application Insights

-              RedGate tools (.net profiler, Monitor)

-              JetBrains dotTrace

-              Datadog

-              And many more

Azure Application Insights is a low-cost tool if you are already on Azure. It has a lot of performance data you can dig into. For example: average duration and count. You can scope it down to a select period.

Get NordVPN - the best VPN service for a safer and faster internet experience. Get up to 77% off + 3 extra months with a 2-year plan! Take your online security to the next level

Get NordVPN - Secure Your Connection

This is an affiliate link. I may earn a commission when you click on it and make a purchase at no extra cost to you.

Azure Sql databases also have Query performance insights. You can see the most executed sql code, the longest running queries and I/O intensive queries and a custom view.

A Semaphore can be used to control access to a pool of resources. If you want to execute some function calls in parallel, you can use a Semaphore to control how many units of work (or threads) that can be executed at once. Doing work in parallel can be very useful if you are doing the same functions on different databases, and/or your work is mostly CPU intensive.  

Keep in mind that you probably must do a lot of testing to find a good balance, because doing too much at the same time can cause your application to run out of resources. Or it can cause your SQL Server to be overloaded.  

Get NordVPN - the best VPN service for a safer and faster internet experience. Get up to 77% off + 3 extra months with a 2-year plan! Take your online security to the next level

Get NordVPN - Secure Your Connection

This is an affiliate link. I may earn a commission when you click on it and make a purchase at no extra cost to you.

Another important thought to keep in mind is that what works great in your development environment might not work as well in your production environment. I have done this mistake myself a couple of times. It turned out that when my code was running in production, with lots of use from customers, I overloaded the SQL Server and everything got slow. So testing is crucial.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace SemaphoreSlimExample
{
    class Program
    {
        static async Task Main(string[] args)
        {
            // Sample work items (could be URLs, files, etc.)
            var items = Enumerable.Range(1, 20).Select(i => $"Item {i}");

            // Process up to 5 items in parallel
            await ProcessInParallelAsync(items, maxDegreeOfParallelism: 5);

            Console.WriteLine("All work completed.");
        }

        static async Task ProcessInParallelAsync(IEnumerable<string> items, int maxDegreeOfParallelism)
        {
            // SemaphoreSlim throttles the number of concurrent tasks
            using var semaphore = new SemaphoreSlim(initialCount: maxDegreeOfParallelism);

            // Create a task for each item
            var tasks = items.Select(async item =>
            {
                // Wait for an available “slot”
                await semaphore.WaitAsync();
                try
                {
                    // Your actual work goes here
                    await DoWorkAsync(item);
                }
                finally
                {
                    // Always release the slot, even on exceptions
                    semaphore.Release();
                }
            });

            // Wait for all tasks to complete
            await Task.WhenAll(tasks);
        }

        static async Task DoWorkAsync(string item)
        {
            // Simulate some asynchronous work
            Console.WriteLine($"Starting {item} on thread {Thread.CurrentThread.ManagedThreadId}");
            await Task.Delay(TimeSpan.FromSeconds(new Random().NextDouble() * 2));
            Console.WriteLine($"Finished {item}");
        }
    }
}

 I am using the SemaphoreSlim, because it is a lightweight implementation and simple to use. If you need cross-process coordination or named semaphores— you can use Semaphore.

Why this pattern?

  • ThrottlingSemaphoreSlim(initialCount) controls how many tasks can enter the “critical section” simultaneously—in this case, maxDegreeOfParallelism.

  • Asynchronous waitingawait semaphore.WaitAsync() doesn’t block a thread; it lets the runtime schedule other work.

  • Safety: Using try…finally ensures that even if DoWorkAsync throws, semaphore.Release() is always called, preventing deadlocks.

  • Disposal: Wrapping SemaphoreSlim in a using ensures its internal wait handles are cleaned up when you’re done.

You can tweak:

  • maxDegreeOfParallelism to match CPU cores or external resource limits.

  • The work delegate to do HTTP calls, database operations, file I/O, etc.

  • Add cancellation by passing a CancellationToken to both WaitAsync and your work.

In short, keeping your site or API responsive means first pinpointing the heaviest hitters with profiling tools like Azure Application Insights or dotTrace, then carefully introducing parallelism—SemaphoreSlim is ideal for throttling simultaneous work—to speed up CPU‐bound or I/O‐bound operations. Always validate your settings under realistic load (not just in development) to avoid overloading your database or servers. With measured tuning and thorough testing, you’ll ensure a fast, reliable experience for every user.

Get NordVPN - the best VPN service for a safer and faster internet experience. Get up to 77% off + 3 extra months with a 2-year plan! Take your online security to the next level

Get NordVPN - Secure Your Connection

This is an affiliate link. I may earn a commission when you click on it and make a purchase at no extra cost to you.

5/9/2025
Related Posts
Entity framework gotcha you didn’t know about

Entity framework gotcha you didn’t know about

Performance issue encountered with Entity Framework Core where a query using FirstOrDefaultAsync and multiple Include statements was unexpectedly slow. By simply moving the filter condition .Where(d => d.Id == doc.Id) earlier in the query chain, EF generated a much more efficient SQL plan, reducing execution time from 20 seconds to milliseconds. The key lesson is that EF sometimes needs a bit of guidance to optimize queries effectively.

Read Full Story
Multitenancy and .net DbContext

Multitenancy and .net DbContext

Read Full Story
Plan, Automate, Review, Repeat

Plan, Automate, Review, Repeat

A practical guide to building a solid website maintenance plan — combining automation, manual reviews, and smart tooling to stay secure, up-to-date, and efficient.

Read Full Story
© Andreas Skeie Hjelle 2025