RateLimit Client - HTTP Traffic Shaping for C#
A C# implementation of a HttpMessageHandler that provides HTTP traffic shaping based on rate limit headers according to the IETF draft-ietf-httpapi-ratelimit-headers specification.
Features
- ✅ Automatic Rate Limit Detection: Parses
RateLimitandRateLimit-Policyheaders from HTTP responses - ✅ Traffic Shaping: Automatically delays requests to stay within rate limits
- ✅ Partition Key Support: Track separate rate limits per user/tenant using partition keys (pk parameter)
- ✅ JWT Token Integration: Extract user IDs from JWT tokens (oid claim) for per-user rate limiting
- ✅ Proactive Throttling: Gradually slows down requests when approaching quota limits
- ✅ Retry-After Support: Respects
Retry-Afterheaders (takes precedence per spec) - ✅ 429 Auto-Retry: Optionally retry requests that receive 429 Too Many Requests
- ✅ Multiple Policies: Supports multiple rate limit policies per endpoint
- ✅ Thread-Safe: Concurrent request handling with proper synchronization
- ✅ Flexible Configuration: Extensive options for customizing behavior
- ✅ Diagnostics: Callbacks and state inspection for monitoring
Quick Start
Basic Usage
using RateLimitClient;
// Create handler with default options
var rateLimitHandler = new RateLimitHandler();
// Use with HttpClient
using var client = new HttpClient(rateLimitHandler);
// Make requests - rate limiting applied automatically
var response = await client.GetAsync("https://api.example.com/data");
Advanced Configuration
var options = new RateLimitHandlerOptions
{
// Wait before sending request (prevents 429 errors)
WaitMode = RateLimitWaitMode.BeforeRequest,
// Enable proactive throttling at 80% quota consumption
EnableProactiveThrottling = true,
ProactiveThrottleThreshold = 0.8,
// Maximum delay to prevent DoS attacks
MaxDelayThreshold = TimeSpan.FromMinutes(2),
// Automatically retry 429 responses
AutoRetryOn429 = true,
// Callback when rate limits are detected
OnRateLimitHeadersReceived = (uri, headers) =>
{
Console.WriteLine($"Rate limit info from {uri.Host}:");
foreach (var limit in headers.Limits)
{
Console.WriteLine($" {limit.PolicyName}: {limit.Remaining} remaining");
}
},
// Callback when throttling occurs
OnDelayCalculated = (uri, delay) =>
{
Console.WriteLine($"Waiting {delay.TotalSeconds:F1}s before next request");
}
};
var handler = new RateLimitHandler(options);
using var client = new HttpClient(handler);
Per-User Rate Limiting with Partition Keys
Partition keys allow tracking separate rate limits for different users or tenants. This is useful when each user has their own quota:
// Extract user ID from JWT token
var token = "Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0...";
var userId = JwtTokenHelper.ExtractOid(token);
// Configure handler
var handler = new RateLimitHandler(new RateLimitHandlerOptions
{
OnRateLimitHeadersReceived = (uri, headers) =>
{
foreach (var limit in headers.Limits)
{
Console.WriteLine($"User {limit.PartitionKey}: {limit.Remaining} requests remaining");
}
}
});
using var client = new HttpClient(handler);
// Add bearer token to request
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
// Server returns headers like:
// RateLimit-Policy: "premium";q=100;w=60;pk="user-12345"
// RateLimit: "premium";r=87;t=45;pk="user-12345"
var response = await client.GetAsync("https://api.example.com/data");
// Check delay for specific user
var tracker = handler.GetTracker();
var delay = tracker.CalculateDelay(
new Uri("https://api.example.com/data"),
userId);
Key Benefits:
- Each user has independent rate limits
- One user hitting their limit doesn't affect others
- Supports different tiers (free, premium, enterprise)
- Partition key automatically tracked from response headers
See PartitionKeyTests.cs in the tests project for complete working demonstrations.
Header Specification
RateLimit-Policy Header
Defines the quota policies available:
RateLimit-Policy: "burst";q=100;w=60, "hourly";q=1000;w=3600
Parameters:
- q (required): Quota allocated in quota units
- w (optional): Time window in seconds
- qu (optional): Quota unit type (
requests,content-bytes,concurrent-requests) - pk (optional): Partition key (byte sequence)
RateLimit Header
Provides current rate limit status:
RateLimit: "burst";r=42;t=15
Parameters:
- r (required): Remaining quota units
- t (optional): Seconds until quota restoration
- pk (optional): Partition key (byte sequence)
Partition Key Format
Partition keys are byte sequences per RFC 9651. They are formatted as :base64data::
RateLimit-Policy: "premium";q=100;w=60;pk=:dXNlci0xMjM0NQ==:
RateLimit: "premium";r=87;t=45;pk=:dXNlci0xMjM0NQ==:
The library automatically:
- Parses byte sequences by decoding base64 between colons
- Serializes partition keys as base64-encoded UTF-8 bytes wrapped in colons
Example partition key encoding:
- Value:
user-12345 - UTF-8 bytes:
75 73 65 72 2d 31 32 33 34 35 - Base64:
dXNlci0xMjM0NQ== - Serialized:
:dXNlci0xMjM0NQ==:
Retry-After Header
Standard HTTP header that takes precedence over rate limit headers:
Retry-After: 120
Configuration Options
| Option | Default | Description |
|---|---|---|
WaitMode |
BeforeRequest |
When to apply delays: BeforeRequest, AfterResponse, or Never |
MaxDelayThreshold |
5 minutes | Maximum delay to prevent DoS attacks |
EnableProactiveThrottling |
true |
Gradually slow requests when approaching limits |
ProactiveThrottleThreshold |
0.8 | Threshold (0-1) for triggering proactive throttling |
StateExpirationTime |
1 hour | How long rate limit state remains valid |
AutoRetryOn429 |
false |
Automatically retry 429 Too Many Requests |
GetLimitKey |
Host-based | Custom function to determine tracking key |
Callbacks
OnRateLimitHeadersReceived
Invoked when rate limit headers are parsed:
OnRateLimitHeadersReceived = (uri, headers) =>
{
// Access parsed headers
foreach (var policy in headers.Policies)
{
Console.WriteLine($"Policy: {policy.Name}, Quota: {policy.Quota}");
}
}
OnDelayCalculated
Invoked when a delay is calculated:
OnDelayCalculated = (uri, delay) =>
{
Console.WriteLine($"Throttling for {delay.TotalSeconds}s");
}
OnTooManyRequests
Invoked when a 429 response is received:
OnTooManyRequests = (request, response) =>
{
Console.WriteLine($"Rate limited: {request.RequestUri}");
}
OnParsingError
Invoked when header parsing fails:
OnParsingError = (uri, exception) =>
{
Console.WriteLine($"Parse error: {exception.Message}");
}
Traffic Shaping Strategies
1. Prevent 429 Errors (Default)
WaitMode = RateLimitWaitMode.BeforeRequest
Waits before sending requests to prevent hitting rate limits.
2. Learn and Adapt
WaitMode = RateLimitWaitMode.AfterResponse
Sends request first, then waits before the next one based on response.
3. Manual Control
WaitMode = RateLimitWaitMode.Never
No automatic delays. Use callbacks and GetTracker() for manual control:
var tracker = handler.GetTracker();
var delay = tracker.CalculateDelay(uri);
if (delay > TimeSpan.Zero)
{
await Task.Delay(delay);
}
Proactive Throttling
When enabled, the handler gradually slows requests as quota is consumed:
- Below threshold: No throttling
- Above threshold: Spreads remaining requests evenly over remaining time
- Example: With 10 requests remaining in 100 seconds, each request waits ~10 seconds
This prevents sudden quota exhaustion and provides smoother traffic patterns.
State Management
Inspect Current State
var tracker = handler.GetTracker();
var state = tracker.GetCurrentState();
foreach (var kvp in state)
{
Console.WriteLine($"{kvp.Key}: {kvp.Value.Remaining} remaining");
}
Clear State
handler.ClearState();
Best Practices
- Use per-host tracking (default) for most scenarios
- Enable proactive throttling to avoid sudden quota exhaustion
- Set reasonable MaxDelayThreshold to prevent excessive waits
- Use callbacks for logging and monitoring
- Handle AutoRetryOn429 carefully - it can cause long delays
- Test with real APIs to tune ProactiveThrottleThreshold
Thread Safety
All components are thread-safe and support concurrent requests. Rate limit state is tracked using thread-safe collections with proper locking.
Spec Compliance
This implementation follows the IETF draft specification:
- ✅ Parses structured field list format
- ✅ Handles multiple policies and limits
- ✅ Respects Retry-After precedence
- ✅ Ignores malformed headers per spec
- ✅ Uses delay-seconds (not timestamps) for reset
- ✅ Implements client-side delay/throttling strategies
- ✅ Accounts for network latency
- ✅ Applies maximum delay thresholds
Project Structure
RateLimitClient/
├── src/
│ └── RateLimitClient/ # Core library
│ ├── RateLimitModels.cs # Data models
│ ├── RateLimitHeaderParser.cs # Header parser
│ ├── RateLimitTracker.cs # State tracking
│ ├── RateLimitHandler.cs # Main handler
│ ├── JwtTokenHelper.cs # JWT utilities
│ └── StructuredFields/ # RFC 9651 parsing (decoupled)
│ ├── IStructuredFieldParser.cs
│ ├── StructuredFieldParser.cs
│ └── StructuredFieldItem.cs
└── tests/
└── RateLimitClient.Tests/ # Test suite
├── BasicUsageTests.cs # Basic usage tests
├── AdvancedUsageTests.cs # Advanced options tests
├── ManualControlTests.cs # Manual control tests
├── PartitionKeyTests.cs # Per-user rate limiting tests
├── ByteSequenceTests.cs # Byte sequence format tests
├── StructuredFieldParserTests.cs # RFC 9651 parser tests
└── MockRateLimitHandler.cs # Mock for testing
Structured Fields Abstraction
The structured fields parsing logic (RFC 9651) has been decoupled into a separate namespace with a clean interface. This allows easy replacement with third-party libraries if needed. See StructuredFields-README.md for details on swapping implementations.
Building
# Restore dependencies
dotnet restore
# Build the solution
dotnet build
# Run tests
dotnet test
# Build library only
dotnet build src/RateLimitClient/RateLimitClient.csproj
Testing
The test suite includes 51+ tests covering:
- Basic usage scenarios
- Advanced configuration options
- Manual rate limit control
- Per-user rate limiting with partition keys
- JWT token integration
- Callback functionality
- State management
- Byte sequence encoding/decoding (RFC 9651)
- Structured field parsing (RFC 9651)
Run tests with:
dotnet test
Or run specific test classes:
dotnet test --filter "FullyQualifiedName~BasicUsageTests"
dotnet test --filter "FullyQualifiedName~PartitionKeyTests"
dotnet test --filter "FullyQualifiedName~StructuredFieldParserTests"
License
This implementation is provided as-is for use in your projects.
API Reference
- API Documentation - Complete API reference