Redis-backed permit acquirer for ThrottleChannel distributed throttling.
$ dotnet add package ThrottleChannel.RedisChannel-driven rate gate with capacity tokens, zero-loss cancellation, deterministic shutdown, and optional Redis-backed distributed coordination.
Stopwatch scheduling guarantees ≤ configured RPS without timer drift.DisposeAsync drains the channel, flags waiters, and surfaces OperationCanceledException.RateGateMetricsSnapshot.ThrottleChannel.Redis ships a fixed-window distributed permit acquirer using standard
ConnectionMultiplexer patterns.dotnet add package ThrottleChannel
# optional distributed integration
dotnet add package ThrottleChannel.Redis
Target frameworks: net8.0, netstandard2.1.
using System.Diagnostics.Metrics;
using ThrottleChannel.RateGate;
using var meter = new Meter("ThrottleChannel.RateGate", "1.0.0");
await using var gate = new FixedRpsGate(
rps: 1,
capacity: 1024,
meter: meter);
foreach (var request in requests)
{
await gate.WaitAsync(request.CancellationToken);
await ProcessAsync(request);
}
var metrics = gate.Metrics;
Console.WriteLine($"Granted: {metrics.TotalGranted}, cancelled: {metrics.TotalCancelled}");
using Microsoft.Extensions.DependencyInjection;
using ThrottleChannel.Distributed;
using ThrottleChannel.RateGate;
using ThrottleChannel.Redis;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddThrottleChannelRedis(
configuration: builder.Configuration.GetConnectionString("redis")!,
configure: options =>
{
options.Key = "throttle:orders";
options.PermitsPerWindow = 1;
});
builder.Services.AddSingleton<FixedRpsGate>(sp =>
{
var acquirer = sp.GetRequiredService<IDistributedPermitAcquirer>();
return new FixedRpsGate(rps: 1, capacity: 1024, distributedAcquirer: acquirer);
});
builder.Services.AddSingleton<IRateGate>(sp => sp.GetRequiredService<FixedRpsGate>());
var app = builder.Build();
app.MapPost("/checkout", async (IRateGate gate, CancellationToken ct) =>
{
await gate.WaitAsync(ct);
return Results.Ok();
});
app.Run();
FixedRpsGate is registered as a singleton, so the DI container disposes it gracefully on shutdown.
Pass a Meter instance (or configure FixedRpsGateFactory with one) to enable automatic publishing through
RateGateMetricsPublisher. Metrics are emitted on the ThrottleChannel.RateGate meter.
The tests/ThrottleChannel.ServiceDefaults project wires OpenTelemetry exporters. When
OTEL_EXPORTER_OTLP_ENDPOINT is set (for example, http://localhost:4317 exposed by the Aspire dashboard), metrics
and traces flow to that endpoint.
Run the local Aspire orchestrator to see live telemetry and distributed coordination:
dotnet run --project tests/ThrottleChannel.AppHost/ThrottleChannel.AppHost.csproj
The orchestrator provisions Redis, boots the integration worker, and links the OpenTelemetry pipeline to the Aspire dashboard.
WaitAsync throws OperationCanceledException when the caller supplies a canceled token.OperationCanceledException.DisposeAsync) cancels every pending waiter, keeping channel and semaphore in a consistent state.| Concern | Queue + lock | Channel + capacity |
|---|---|---|
| Backpressure | Full queue drops items or blocks producers | SemaphoreSlim gates new writers without touching the channel |
| Cancellation | Requires manual removal and lock convoy | Token callback flips the TCS and releases capacity instantly |
| Throughput pacing | Timer drift accumulates | Monotonic Stopwatch timestamps keep ≤ configured RPS |
| Shutdown | Requires walk over queue under lock | Reader naturally drains remaining work with deterministic cancel |
Scenario: 1k permit acquisitions per run, 1 or 10 rps configuration, averaged over 5 iterations. Baseline is legacy linked-list queue + locks (simulated in benchmarks project).
| RPS | Legacy gate (mean µs) | Channel gate (mean µs) | Δ |
|---|---|---|---|
| 1 | (collect after running dotnet run -c Release in benchmarks) | (collect after running benchmarks) | (pending) |
| 10 | (collect after running dotnet run -c Release in benchmarks) | (collect after running benchmarks) | (pending) |
Run locally:
cd benchmarks/ThrottleChannel.Benchmarks
dotnet run -c Release
Benchmarks require restoring NuGet packages. In restricted CI environments provide an offline cache or enable network access.
Redis integration suites rely on Docker via Testcontainers; the tests spin up a disposable Redis container and skip only if the local Docker daemon is unavailable. Make sure Docker Desktop/Colima/Podman is running before executing:
dotnet test ThrottleChannel.sln
ci.yml — restore, build, test (with coverage), pack artifacts on every push/PR.release.yml — publish signed packages to NuGet when tagging v*.*.* (requires NUGET_API_KEY).src/ThrottleChannel — core channel-based gate.src/ThrottleChannel.Redis — Redis permit acquirer + DI helpers.tests/ThrottleChannel.Tests — xUnit coverage for cancellation, capacity, pacing, shutdown.tests/ThrottleChannel.TestSupport — Redis Testcontainers helpers and conditional attributes.tests/ThrottleChannel.ServiceDefaults — shared resilience + OpenTelemetry defaults for Aspire-integrated services.benchmarks/ThrottleChannel.Benchmarks — BenchmarkDotNet comparisons.samples/BasicConsole — console demo with multiple producers under 1 RPS gate.tests/ThrottleChannel.AppHost — Aspire orchestrator for integration scenarios.tests/ThrottleChannel.IntegrationWorker — background load generator used in Aspire orchestration.See AGENTS.md for the full backlog: token bucket, per-key limits, sliding window, concurrency caps, telemetry hooks,
NativeAOT.