Course contentsShow
← C#
Lesson 1566 of 1,90814. Background Work and MessagingFree lesson

Adaptive Rate Limiting

Implement self-adjusting rate limits that respond to system resource availability.

Adaptive Rate Limiting

In this lesson, you will learn how to design a background consumer that dynamically adjusts its processing throughput based on real-time system health metrics.

Beyond Static Throttling

Static rate limiting—where you hardcode a fixed number of messages per second—is often inefficient in complex event-driven architectures. If your background worker is processing messages while the CPU is spiking or memory is nearing capacity, a static limit can inadvertently crash the system. Adaptive Rate Limiting allows your application to "listen" to its own pulse and scale its consumption rate up or down automatically.

The core idea is to introduce a feedback loop: observe resource telemetry (like System.Diagnostics.PerformanceCounter or memory pressure) and adjust the SemaphoreSlim or RateLimiter instances accordingly.

Implementing the Feedback Loop

Instead of a fixed limit, we use an observable property to gate our throughput. Consider this simplified implementation:

public class AdaptiveConsumer
{
    private int _maxConcurrency = 10;
    private readonly SemaphoreSlim _semaphore = new(10);

    public async Task ProcessMessageAsync(Message msg)
    {
        // Adjust throughput based on system pressure
        await AdjustConcurrencyAsync();

        await _semaphore.WaitAsync();
        try 
        {
            await Task.Run(() => Handle(msg));
        }
        finally 
        {
            _semaphore.Release();
        }
    }

    private async Task AdjustConcurrencyAsync()
    {
        var cpuLoad = GetCurrentCpuLoad(); // Simplified telemetry
        
        if (cpuLoad > 80 && _maxConcurrency > 1) 
            _maxConcurrency--;
        else if (cpuLoad < 50 && _maxConcurrency < 50) 
            _maxConcurrency++;
            
        // Implementation note: Updating semaphore capacity dynamically
    }
}

In this pattern, the consumer acts as a self-regulating agent. When the system is healthy, it increases its concurrency to maximize throughput; when pressure mounts, it shrinks the concurrency window to allow the system breathing room. This ensures that your background tasks don't compete with the primary application threads for critical resources during peak traffic.

By tying your message consumption to actual resource availability, you prevent the common "death spiral" where overloaded systems attempt to process more work, only to fail, retry, and increase the load further.

Key Takeaway: Adaptive rate limiting transforms your background workers from static consumers into resource-aware processes that prioritize system stability over raw throughput.