Grove
Extensions

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:

ParameterTypeDescription
store*kv.StoreThe backing KV store
prefixstringKey prefix for rate limit counters
rateintMaximum number of allowed requests per window
windowtime.DurationDuration 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) error
err := 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
}
FieldTypeDescription
Allowedbooltrue if the request is within the rate limit
RemainingintNumber of requests remaining before the limit is reached (0 when exceeded)
ResetAttime.TimeTimestamp when the current window expires and the counter resets

How It Works

The rate limiter uses a sliding window counter approach:

  1. On each Allow call, the current count for the key is read from the store.
  2. If the count is at or above the configured rate, the request is denied (Allowed = false).
  3. Otherwise, the counter is incremented and written back with a TTL equal to the window duration.
  4. 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/orders

Each key has a TTL equal to the configured window, so counters are automatically cleaned up when the window expires.

On this page