Rate Limiter
January 21, 2026 ยท View on GitHub
Implements IP and path-based rate limiting at the edge using Fastly's Edge Rate Limiting (ERL) primitives. Blocks abusive clients before they reach your origin, with different limits for different endpoints.
Source Code
const std = @import("std");
const zigly = @import("zigly");
const erl = zigly.erl;
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
var downstream = try zigly.downstream();
// Get client IP
const client_ip = try zigly.http.Downstream.getClientIpAddr();
const ip_str = try client_ip.print(allocator);
// Get request path for endpoint-specific rate limiting
var uri_buf: [4096]u8 = undefined;
const path = try downstream.request.getPath(&uri_buf);
// Choose rate limit based on endpoint sensitivity
const config: struct { counter: []const u8, limit: u32, window: u32 } = if (std.mem.startsWith(u8, path, "/api/auth/") or
std.mem.startsWith(u8, path, "/api/login"))
.{
// Strict limits for auth endpoints: 10 requests per minute
.counter = "auth_requests",
.limit = 10,
.window = 60,
} else if (std.mem.startsWith(u8, path, "/api/")) .{
// Moderate limits for API: 100 requests per minute
.counter = "api_requests",
.limit = 100,
.window = 60,
} else .{
// Relaxed limits for static content: 500 requests per minute
.counter = "general_requests",
.limit = 500,
.window = 60,
};
// Build rate limiter key combining IP and endpoint category
const key = try std.fmt.allocPrint(allocator, "{s}:{s}", .{ ip_str, config.counter });
const limiter = erl.RateLimiter.init(.{
.rate_counter = config.counter,
.penalty_box = "blocked_ips",
.window_seconds = config.window,
.limit = config.limit,
.ttl_seconds = 300,
});
// Check rate limit
if (try limiter.isBlocked(key, 1)) {
try downstream.response.setStatus(429);
try downstream.response.headers.set("Content-Type", "application/json");
try downstream.response.headers.set("Retry-After", "60");
try downstream.response.body.writeAll("{\"error\":\"Rate limit exceeded\",\"retry_after\":60}");
try downstream.response.finish();
return;
}
// Request allowed, proxy to origin
try downstream.proxy("origin", null);
}
How It Works
- Get the client IP address using
Downstream.getClientIpAddr() - Extract the request path using
getPath()for endpoint-specific limits - Select rate limit configuration based on path:
- Auth endpoints (
/api/auth/,/api/login): 10 requests per minute - API endpoints (
/api/): 100 requests per minute - General content: 500 requests per minute
- Auth endpoints (
- Build a composite key combining IP and endpoint category
- Configure a
RateLimiterwith the selected limits - Check if the request should be blocked using
isBlocked() - Return 429 with
Retry-Afterheader if blocked, otherwise proxy the request
ERL Configuration
In fastly.toml for local development:
[local_server.rate_counter.auth_requests]
[local_server.rate_counter.api_requests]
[local_server.rate_counter.general_requests]
[local_server.penalty_box.blocked_ips]
No additional configuration needed for production - Fastly provides the rate counter and penalty box infrastructure.
Testing
# Test general endpoint (500/min limit)
curl http://127.0.0.1:7676/
# Test API endpoint (100/min limit)
curl http://127.0.0.1:7676/api/data
# Test auth endpoint (10/min limit) - will hit limit quickly
for i in {1..15}; do curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:7676/api/auth/login; done
Variations
Simple IP-only rate limiting:
const limiter = erl.RateLimiter.init(.{
.rate_counter = "ip_requests",
.penalty_box = "blocked_ips",
.window_seconds = 60,
.limit = 100,
.ttl_seconds = 300,
});
if (try limiter.isBlocked(ip_str, 1)) {
// Return 429
}
Tiered rate limits by authentication:
var auth_buf: [256]u8 = undefined;
if (downstream.request.headers.get("Authorization", &auth_buf)) |_| {
const premium_limiter = erl.RateLimiter.init(.{
.rate_counter = "premium_requests",
.penalty_box = "premium_blocked",
.window_seconds = 60,
.limit = 1000,
.ttl_seconds = 60,
});
if (try premium_limiter.isBlocked(ip_str, 1)) {
// Return 429
}
} else {
// Use standard limiter for unauthenticated requests
}
Manual counter management:
// Just count without automatic blocking
var counter = erl.RateCounter.open("request_counter");
const count = try counter.increment(ip_str, 1);
if (count > 50) {
// Log high-traffic IPs without blocking
var logger = try zigly.logger.Logger.open("analytics");
try logger.write(ip_str);
}