Throttling Patterns
Throttling limits the rate of operations to prevent overwhelming resources. This is essential for API calls, database queries, and any operation with rate limits.
Why Throttle?
Without throttling:
- APIs return rate limit errors (HTTP 429)
- Databases become overloaded
- Network connections exhaust
- Memory usage spikes
- Systems become unresponsive
SemaphoreSlim for Concurrency Limiting
The simplest throttling approach - limit concurrent operations:
csharp1class ThrottledClient2{3 private readonly SemaphoreSlim _throttle;4 private readonly HttpClient _client = new();56 public ThrottledClient(int maxConcurrent)7 {8 _throttle = new SemaphoreSlim(maxConcurrent);9 }1011 public async Task<string> GetAsync(string url)12 {13 await _throttle.WaitAsync();14 try15 {16 return await _client.GetStringAsync(url);17 }18 finally19 {20 _throttle.Release();21 }22 }23}2425// Usage: Max 5 concurrent requests26var client = new ThrottledClient(5);27var tasks = urls.Select(url => client.GetAsync(url));28var results = await Task.WhenAll(tasks);
Parallel.ForEachAsync with MaxDegreeOfParallelism
.NET 6+ provides built-in parallel throttling:
csharp1var options = new ParallelOptions2{3 MaxDegreeOfParallelism = 5 // Max 5 concurrent4};56await Parallel.ForEachAsync(urls, options, async (url, ct) =>7{8 var data = await httpClient.GetStringAsync(url, ct);9 Console.WriteLine($"Downloaded: {url}");10});
Rate Limiting (Requests per Second)
Limit the rate, not just concurrency:
csharp1class RateLimiter2{3 private readonly SemaphoreSlim _semaphore;4 private readonly TimeSpan _interval;56 public RateLimiter(int maxRequestsPerSecond)7 {8 _semaphore = new SemaphoreSlim(maxRequestsPerSecond);9 _interval = TimeSpan.FromSeconds(1.0 / maxRequestsPerSecond);10 }1112 public async Task<T> ExecuteAsync<T>(Func<Task<T>> operation)13 {14 await _semaphore.WaitAsync();1516 try17 {18 return await operation();19 }20 finally21 {22 // Release after interval, not immediately23 _ = ReleaseAfterDelay();24 }25 }2627 private async Task ReleaseAfterDelay()28 {29 await Task.Delay(_interval);30 _semaphore.Release();31 }32}3334// Usage: Max 10 requests per second35var limiter = new RateLimiter(10);36foreach (var url in urls)37{38 var result = await limiter.ExecuteAsync(() => httpClient.GetStringAsync(url));39}
Token Bucket Pattern
Classic rate limiting algorithm:
csharp1class TokenBucket2{3 private readonly int _maxTokens;4 private readonly double _tokensPerSecond;5 private double _tokens;6 private DateTime _lastRefill;7 private readonly object _lock = new();89 public TokenBucket(int maxTokens, double tokensPerSecond)10 {11 _maxTokens = maxTokens;12 _tokensPerSecond = tokensPerSecond;13 _tokens = maxTokens;14 _lastRefill = DateTime.UtcNow;15 }1617 public bool TryConsume(int tokens = 1)18 {19 lock (_lock)20 {21 Refill();2223 if (_tokens >= tokens)24 {25 _tokens -= tokens;26 return true;27 }2829 return false;30 }31 }3233 private void Refill()34 {35 var now = DateTime.UtcNow;36 var elapsed = (now - _lastRefill).TotalSeconds;37 var newTokens = elapsed * _tokensPerSecond;38 _tokens = Math.Min(_maxTokens, _tokens + newTokens);39 _lastRefill = now;40 }41}4243// Usage44var bucket = new TokenBucket(maxTokens: 10, tokensPerSecond: 2);4546while (hasWork)47{48 if (bucket.TryConsume())49 {50 await DoWorkAsync();51 }52 else53 {54 await Task.Delay(100); // Wait for tokens55 }56}
Sliding Window Pattern
Track requests in a time window:
csharp1class SlidingWindowLimiter2{3 private readonly int _maxRequests;4 private readonly TimeSpan _window;5 private readonly Queue<DateTime> _timestamps = new();6 private readonly object _lock = new();78 public SlidingWindowLimiter(int maxRequests, TimeSpan window)9 {10 _maxRequests = maxRequests;11 _window = window;12 }1314 public bool TryAcquire()15 {16 lock (_lock)17 {18 var now = DateTime.UtcNow;19 var windowStart = now - _window;2021 // Remove expired timestamps22 while (_timestamps.Count > 0 && _timestamps.Peek() < windowStart)23 {24 _timestamps.Dequeue();25 }2627 if (_timestamps.Count < _maxRequests)28 {29 _timestamps.Enqueue(now);30 return true;31 }3233 return false;34 }35 }36}3738// Usage: Max 100 requests per minute39var limiter = new SlidingWindowLimiter(100, TimeSpan.FromMinutes(1));
Batch Processing
Process items in batches to reduce overhead:
csharp1async Task ProcessInBatchesAsync<T>(2 IEnumerable<T> items,3 int batchSize,4 Func<T[], Task> processBatch)5{6 var batch = new List<T>(batchSize);78 foreach (var item in items)9 {10 batch.Add(item);1112 if (batch.Count >= batchSize)13 {14 await processBatch(batch.ToArray());15 batch.Clear();16 }17 }1819 // Process remaining items20 if (batch.Count > 0)21 {22 await processBatch(batch.ToArray());23 }24}2526// Usage27await ProcessInBatchesAsync(28 records,29 batchSize: 100,30 async batch => await database.BulkInsertAsync(batch)31);
Combining Throttling Strategies
csharp1class SmartThrottler2{3 private readonly SemaphoreSlim _concurrencyLimiter;4 private readonly SlidingWindowLimiter _rateLimiter;56 public SmartThrottler(int maxConcurrent, int maxPerMinute)7 {8 _concurrencyLimiter = new SemaphoreSlim(maxConcurrent);9 _rateLimiter = new SlidingWindowLimiter(maxPerMinute, TimeSpan.FromMinutes(1));10 }1112 public async Task<T> ExecuteAsync<T>(Func<Task<T>> operation)13 {14 // Wait for rate limit15 while (!_rateLimiter.TryAcquire())16 {17 await Task.Delay(100);18 }1920 // Wait for concurrency slot21 await _concurrencyLimiter.WaitAsync();22 try23 {24 return await operation();25 }26 finally27 {28 _concurrencyLimiter.Release();29 }30 }31}
Built-in Rate Limiting (.NET 7+)
.NET 7 introduces System.Threading.RateLimiting:
csharp1using System.Threading.RateLimiting;23// Token bucket limiter4var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions5{6 TokenLimit = 10,7 QueueLimit = 100,8 ReplenishmentPeriod = TimeSpan.FromSeconds(1),9 TokensPerPeriod = 210});1112// Usage13using var lease = await limiter.AcquireAsync(permitCount: 1);14if (lease.IsAcquired)15{16 await DoWorkAsync();17}
Key Takeaways
- Use
SemaphoreSlimfor simple concurrency limiting - Use
Parallel.ForEachAsyncwithMaxDegreeOfParallelismfor easy throttling - Token bucket allows bursts up to max, then steady rate
- Sliding window enforces strict rate over time period
- Combine concurrency + rate limiting for robust control
- .NET 7+ has built-in rate limiting APIs
- Batch processing reduces overhead for bulk operations