A zero-dependency GCRA-based rate limiter with a token-bucket-like API for Zig 0.16.0+.
- Global limiting: Use
GlobalLimiterwhen you want a single shared limit across all requests (e.g. protect total server throughput). Its state is lock-free and thread-safe; clocks injected withinitWithClockmust also be thread-safe. - Per-key rate limiting: Each key is tracked independently (e.g. per user ID or IP address). The
RateLimiteris not thread-safe. If you share it across multiple threads, you should protect it with astd.Io.Mutex. - Key storage controls: Use
initial_capacityto reserve space up front, and configuremax_entrieswith anidle_timeoutduration to bound memory and reclaim inactive, fully-drained keys. ExplicitpruneExpired()calls return an allocation error if pruning scratch space cannot be reserved. - Blocking vs non-blocking:
allow()→ ImmediateDecision;.allowedhas no payload, while denied results carry astd.Io.DurationallowN(n)→ Atomically consumesnrequests. The maximum batch size is1 + burst; impossible batches returnerror.BatchTooLarge.wait(io, key)→ Blocks until allowed (usesstd.Io.sleep)waitN(io, n)/waitN(io, key, n)→ Blocks until an entire batch is allowed; impossible batches returnerror.BatchTooLarge.
- Clocks:
init(io, config)→ Installs Zig's monotonic.awakeclock automaticallyinitWithClock(config, clock)→ Injects a custom clock for deterministic testsManualClock→ Built-in single-threaded testing clock
const std = @import("std");
const zimit = @import("zimit");
pub fn main(init: std.process.Init) !void {
const gpa = init.gpa;
const io = init.io;
var limiter = try zimit.RateLimiter([]const u8).init(io, .{
.allocator = gpa,
.limit = .perSecond(5),
.burst = 2,
});
defer limiter.deinit();
const key = "127.0.0.1";
var i: usize = 0;
while (i < 5) : (i += 1) {
const decision = try limiter.allow(key);
switch (decision) {
.allowed => std.debug.print("allowed\n", .{}),
.denied => {
std.debug.print("retry in {d}ms\n", .{
decision.retryAfterMillisecondsCeil().?,
});
},
}
}
}
Use Limit.perSecond, Limit.perMinute, or Limit.perHour for common rates.
For an arbitrary period, initialize Limit directly with count and
period, for example .{ .count = 5, .period = .fromSeconds(2) }.
See examples for more.
Compile every example without running it using zig build examples. To run
the examples as a smoke test, including the deliberately paced waiting example,
use zig build smoke.
String keys are copied automatically, and integer keys work directly.
Applications using structured keys with custom equality or memory ownership can
use RateLimiterWithContext. Its Config adds the required context and
ownership fields while retaining the usual init and initWithClock
constructors.
Zimit exposes operation-specific error sets so callers can handle failures exhaustively:
InitializationErrorcovers invalid configuration, unrepresentable time values, and initial keyed-storage allocation.AdmissionErrorcovers invalid batches, time overflow, keyed-storage allocation, and configured key-capacity exhaustion.WaitErrorcontains every admission error pluserror.Canceled, which is returned when cancellation interrupts the underlying I/O sleep.
pruneExpired is narrower still and returns only allocator errors.
Run:
zig fetch --save git+https://github.com/minhqdao/zimit.git#0.2.1Then in your build.zig:
const zimit_dep = b.dependency("zimit", .{
.target = target,
.optimize = optimize,
});
const exe = b.addExecutable(.{
.name = "yourapp",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "zimit", .module = zimit_dep.module("zimit") },
},
}),
});Format all Zig files before committing to prevent the CI from failing:
zig fmt src/ examples/ build.zigInstall ZLS 0.16.0 and ensure zls is available in PATH, then run:
python3 tools/zls_lint.pyThe script sends every project Zig source file to ZLS and fails on any diagnostic, using the same results reported by editor integrations.
Run the test suite with:
zig build testRun the ReleaseFast benchmark suite with zig build bench. It measures global
admission, contended global admission, keyed hits, borrowed and copied new-key
insertion, full-capacity rejection, and pruning. Workload sizes are configurable:
zig build bench -Dbench-iterations=2000000 -Dbench-keys=50000 -Dbench-threads=8