ERL Reference

January 20, 2026 ยท View on GitHub

Edge Rate Limiting (ERL) provides rate counters, penalty boxes, and a combined rate limiter.

RateCounter

Tracks request counts over sliding time windows.

Static Methods

open

pub fn open(name: []const u8) RateCounter

Open a rate counter by name. Creates it if it doesn't exist.

const erl = zigly.erl;

const rc = erl.RateCounter.open("my_counter");

Instance Methods

increment

pub fn increment(self: RateCounter, entry: []const u8, delta: u32) !void

Increment the counter for an entry.

try rc.increment("client-ip", 1);

// Increment by more for weighted counting
try rc.increment("client-ip", 5);

lookupRate

pub fn lookupRate(self: RateCounter, entry: []const u8, window_seconds: u32) !u32

Get the request rate (per second) over a time window.

const rate = try rc.lookupRate("client-ip", 10);  // 10 second window
// rate = requests per second averaged over the window

lookupCount

pub fn lookupCount(self: RateCounter, entry: []const u8, duration_seconds: u32) !u32

Get the total count over a duration.

const count = try rc.lookupCount("client-ip", 60);  // Last 60 seconds

PenaltyBox

Tracks blocked entries with TTL.

Static Methods

open

pub fn open(name: []const u8) PenaltyBox

Open a penalty box by name.

const pb = erl.PenaltyBox.open("blocked_clients");

Instance Methods

add

pub fn add(self: PenaltyBox, entry: []const u8, ttl_seconds: u32) !void

Add an entry to the penalty box.

try pb.add("bad-actor", 300);  // Block for 5 minutes

has

pub fn has(self: PenaltyBox, entry: []const u8) !bool

Check if an entry is in the penalty box.

if (try pb.has("client-ip")) {
    // Client is blocked
}

RateLimiter

Combines rate counter and penalty box for common rate limiting patterns.

Initialization

pub fn init(options: struct {
    rate_counter: []const u8,
    penalty_box: []const u8,
    window_seconds: u32,
    limit: u32,
    ttl_seconds: u32,
}) RateLimiter

Create a rate limiter.

const limiter = erl.RateLimiter.init(.{
    .rate_counter = "requests",     // Counter name
    .penalty_box = "blocked",       // Penalty box name
    .window_seconds = 60,           // 1 minute window
    .limit = 100,                   // 100 requests per window
    .ttl_seconds = 300,             // Block for 5 minutes when exceeded
});

Instance Methods

checkRate

pub fn checkRate(self: RateLimiter, entry: []const u8, delta: u32) !CheckRateResult

Check and increment atomically. Returns .allowed or .blocked.

const result = try limiter.checkRate("client-ip", 1);
if (result == .blocked) {
    // Request is rate limited
}

isAllowed

pub fn isAllowed(self: RateLimiter, entry: []const u8, delta: u32) !bool

Check if the request is allowed.

if (try limiter.isAllowed("client-ip", 1)) {
    // Process request
} else {
    // Rate limited
}

isBlocked

pub fn isBlocked(self: RateLimiter, entry: []const u8, delta: u32) !bool

Check if the request is blocked.

if (try limiter.isBlocked("client-ip", 1)) {
    // Return 429
}

checkRate (Module Function)

Standalone rate check without creating a RateLimiter.

pub fn checkRate(
    rate_counter: []const u8,
    entry: []const u8,
    delta: u32,
    window_seconds: u32,
    limit: u32,
    penalty_box: []const u8,
    ttl_seconds: u32,
) !CheckRateResult
const result = try erl.checkRate(
    "requests",    // rate_counter
    "client-ip",   // entry
    1,             // delta
    60,            // window_seconds
    100,           // limit
    "blocked",     // penalty_box
    300,           // ttl_seconds
);

CheckRateResult

Result of a rate check.

pub const CheckRateResult = enum(u32) {
    allowed = 0,
    blocked = 1,
};

Usage Patterns

IP-Based Rate Limiting

fn start() !void {
    var downstream = try zigly.downstream();

    const client_ip = try zigly.http.Downstream.getClientIpAddr();
    const ip_str = try client_ip.print(allocator);
    defer allocator.free(ip_str);

    const limiter = erl.RateLimiter.init(.{
        .rate_counter = "ip_requests",
        .penalty_box = "ip_blocked",
        .window_seconds = 60,
        .limit = 100,
        .ttl_seconds = 300,
    });

    if (try limiter.isBlocked(ip_str, 1)) {
        try downstream.response.setStatus(429);
        try downstream.response.headers.set("Retry-After", "300");
        try downstream.response.body.writeAll("Rate limit exceeded");
        try downstream.response.finish();
        return;
    }

    try downstream.proxy("origin", null);
}

API Key Rate Limiting

fn start() !void {
    var downstream = try zigly.downstream();

    const api_key = downstream.request.headers.get(allocator, "X-API-Key") catch {
        try downstream.response.setStatus(401);
        try downstream.response.finish();
        return;
    };

    const limiter = erl.RateLimiter.init(.{
        .rate_counter = "api_keys",
        .penalty_box = "api_blocked",
        .window_seconds = 3600,  // 1 hour
        .limit = 10000,          // 10,000/hour
        .ttl_seconds = 3600,
    });

    if (try limiter.isBlocked(api_key, 1)) {
        try downstream.response.setStatus(429);
        try downstream.response.finish();
        return;
    }

    try downstream.proxy("origin", null);
}

Monitoring Without Blocking

fn start() !void {
    var downstream = try zigly.downstream();

    const rc = erl.RateCounter.open("all_requests");
    const client_ip = try zigly.http.Downstream.getClientIpAddr();
    const ip_str = try client_ip.print(allocator);

    // Just count
    try rc.increment(ip_str, 1);

    // Log rate
    const rate = try rc.lookupRate(ip_str, 60);
    std.debug.print("Client rate: {}/min\n", .{rate * 60});

    try downstream.proxy("origin", null);
}