Rate Limiter
Distributed rate limiting with a sliding window counter approach, backed by any Grove KV store.
The RateLimiter extension provides distributed rate limiting using a sliding window counter. It tracks request counts per key and enforces a maximum number of requests within a configurable time window.
Installation
import "github.com/xraph/grove/kv/plugins"Creating a Rate Limiter
limiter := plugins.NewRateLimiter(store, "api", 100, time.Minute)Parameters:
| Parameter | Type | Description |
|---|---|---|
store | *kv.Store | The backing KV store |
prefix | string | Key prefix for rate limit counters |
rate | int | Maximum number of allowed requests per window |
window | time.Duration | Duration of the sliding window |
The limiter stores counters at keys formatted as <prefix>:<key>, with a TTL equal to the window duration. When the window expires, the counter resets automatically.
API Reference
Allow
Checks whether a request identified by key is within the rate limit. If allowed, the counter is incremented.
func (rl *RateLimiter) Allow(ctx context.Context, key string) (*RateLimitResult, error)result, err := limiter.Allow(ctx, "user:u_123")
if err != nil {
// handle error
}
if !result.Allowed {
// rate limit exceeded
}Reset
Clears the rate limit counter for a given key, allowing the full rate again immediately.
func (rl *RateLimiter) Reset(ctx context.Context, key string) errorerr := limiter.Reset(ctx, "user:u_123")RateLimitResult
The Allow method returns a *RateLimitResult struct with the following fields:
type RateLimitResult struct {
Allowed bool // Whether the request is allowed
Remaining int // Number of requests remaining in the current window
ResetAt time.Time // When the current window resets
}| Field | Type | Description |
|---|---|---|
Allowed | bool | true if the request is within the rate limit |
Remaining | int | Number of requests remaining before the limit is reached (0 when exceeded) |
ResetAt | time.Time | Timestamp when the current window expires and the counter resets |
How It Works
The rate limiter uses a sliding window counter approach:
- On each
Allowcall, the current count for the key is read from the store. - If the count is at or above the configured
rate, the request is denied (Allowed = false). - Otherwise, the counter is incremented and written back with a TTL equal to the
windowduration. - When the TTL expires, the key is automatically removed and the counter resets to zero.
Example: API Rate Limiting per User
package main
import (
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/xraph/grove/kv"
"github.com/xraph/grove/kv/plugins"
"github.com/xraph/grove/kv/drivers/redisdriver"
)
func main() {
ctx := context.Background()
rdb := redisdriver.New()
rdb.Open(ctx, "redis://localhost:6379/0")
store, _ := kv.Open(rdb)
defer store.Close()
// 100 requests per minute per user.
limiter := plugins.NewRateLimiter(store, "api", 100, time.Minute)
mux := http.NewServeMux()
mux.HandleFunc("/api/data", RateLimitMiddleware(limiter, handleData))
http.ListenAndServe(":8080", mux)
}
func RateLimitMiddleware(limiter *plugins.RateLimiter, next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Use the client IP as the rate limit key.
key := r.RemoteAddr
result, err := limiter.Allow(r.Context(), key)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
// Set standard rate limit headers.
w.Header().Set("X-RateLimit-Remaining", strconv.Itoa(result.Remaining))
w.Header().Set("X-RateLimit-Reset", strconv.FormatInt(result.ResetAt.Unix(), 10))
if !result.Allowed {
w.Header().Set("Retry-After", strconv.FormatInt(int64(time.Until(result.ResetAt).Seconds()), 10))
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
return
}
next(w, r)
}
}
func handleData(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}Example: Per-Endpoint Rate Limiting
You can create separate limiters for different endpoints or combine the endpoint path into the key:
// Strict limit on auth endpoints.
authLimiter := plugins.NewRateLimiter(store, "auth", 5, time.Minute)
// Generous limit on read endpoints.
readLimiter := plugins.NewRateLimiter(store, "read", 1000, time.Minute)
// Or use a single limiter with composite keys.
limiter := plugins.NewRateLimiter(store, "rl", 100, time.Minute)
// Key includes both user and endpoint.
result, _ := limiter.Allow(ctx, "user:u_123:/api/orders")Key Layout
Rate limit counters are stored with the following key pattern:
<prefix>:<key>For example:
api:user:u_123
auth:192.168.1.1
rl:user:u_123:/api/ordersEach key has a TTL equal to the configured window, so counters are automatically cleaned up when the window expires.