Build a rate limiter with a fixed window
In this tutorial, you build a fixed-window rate limiter that caps how many requests a caller can make in a given time window — for example, 5 requests per 10 seconds. You use an atomic counter so that many callers can be counted at once without losing a count, a per-window key so each window starts fresh, and a TTL so old windows clean themselves up. You need the .NET 8 SDK or later and a text editor.
The whole design rests on one idea: the current window is identified by a key that includes the window number, so incrementing that key both records a request and tells you the running total for the window. Because the increment is atomic, the limit holds even under concurrency. Counters live on IZaris, so you cast the client to IZaris first.
1. Create a project and add the SDK
Create a console project and add the Zaris SDK.
dotnet new console -o zaris-ratelimit
cd zaris-ratelimit
dotnet add package Clustron.Zaris.SDK
2. Get an IZaris client
In Program.cs, register an in-process store and resolve the client as IZaris so the Counters accessor is available.
using Clustron.Zaris.Abstractions;
using Clustron.Zaris.Client;
using Clustron.Zaris.Client.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection()
.AddClustronZaris("demo", "zaris://inproc/demo")
.BuildServiceProvider();
var client = (IZaris)await services
.GetRequiredService<IZarisClientProvider>()
.GetAsync("demo");
3. Compute the current window key
Divide the current time by the window length to get a window number, then build a key that combines the caller and that number. Every request in the same 10-second slice maps to the same key; the next slice maps to a new key that starts counting from zero.
var windowSeconds = 10;
string WindowKey(string caller)
{
var windowId = DateTimeOffset.UtcNow.ToUnixTimeSeconds() / windowSeconds;
return $"ratelimit:{caller}:{windowId}";
}
Embedding the window number in the key is what "resets" the window: you never clear a counter, you simply move to a new key when time advances.
4. Count a request and decide allow or deny
Increment the window's counter with Counters.AddAsync. The return value's Value.Current is the running total after this request. Compare it against your limit: at or below the limit the request is allowed, above it the request is denied. Pass CounterOptions.Ttl so each window key expires shortly after its window ends and old keys do not pile up.
var limit = 5;
async Task<bool> TryRequestAsync(string caller)
{
var key = WindowKey(caller);
var result = await client.Counters.AddAsync(
key,
1,
new CounterOptions { Ttl = TimeSpan.FromSeconds(windowSeconds * 2) });
if (!result.IsSuccess)
return false; // fail closed if the counter could not be updated
var count = result.Value.Current;
return count <= limit;
}
Because AddAsync applies the increment atomically on the server, two callers that arrive at the same instant get two distinct totals — no request is ever counted twice or missed, so the limit is exact.
5. Drive the limiter
Fire more requests than the limit allows within one window and watch the limiter switch from allow to deny once the count passes the limit.
for (int i = 1; i <= 8; i++)
{
var allowed = await TryRequestAsync("alice");
Console.WriteLine($"Request {i}: {(allowed ? "ALLOW" : "DENY")}");
}
With a limit of 5, the first five requests print ALLOW and the rest print DENY. Wait longer than the window and the next request lands on a new window key, so the count restarts and the caller is allowed again.
6. Confirm the window resets
Wait for the current window to elapse, then make one more request. It falls into a fresh window key that starts at 1, so it is allowed.
Console.WriteLine("Waiting for the window to roll over...");
await Task.Delay(TimeSpan.FromSeconds(windowSeconds));
var afterReset = await TryRequestAsync("alice");
Console.WriteLine($"After reset: {(afterReset ? "ALLOW" : "DENY")}");
The request prints ALLOW because time advanced into a new window, producing a new key whose count begins at zero.
What you built
You built a fixed-window rate limiter that holds its limit exactly, even when many callers hit it at once. The counter's atomic AddAsync gives each request a unique running total, so Value.Current compared against the limit is a reliable allow/deny decision. Putting the window number in the key means each window starts fresh with no reset step, and CounterOptions.Ttl lets expired windows clean themselves up. To rate-limit a different caller, change the caller portion of the key; to change the window or limit, adjust windowSeconds and limit.