惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Jina AI
Jina AI
C
Cisco Blogs
博客园 - Franky
有赞技术团队
有赞技术团队
雷峰网
雷峰网
博客园 - 聂微东
WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
NISL@THU
NISL@THU
P
Palo Alto Networks Blog
博客园_首页
阮一峰的网络日志
阮一峰的网络日志
美团技术团队
Hugging Face - Blog
Hugging Face - Blog
宝玉的分享
宝玉的分享
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Threat Research - Cisco Blogs
T
Tailwind CSS Blog
T
Threatpost
D
Darknet – Hacking Tools, Hacker News & Cyber Security
W
WeLiveSecurity
大猫的无限游戏
大猫的无限游戏
V
Visual Studio Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
SegmentFault 最新的问题
博客园 - 【当耐特】
爱范儿
爱范儿
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
A
Arctic Wolf
PCI Perspectives
PCI Perspectives
S
Secure Thoughts
The Cloudflare Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
L
LINUX DO - 最新话题
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
博客园 - 三生石上(FineUI控件)
J
Java Code Geeks
The Last Watchdog
The Last Watchdog
人人都是产品经理
人人都是产品经理
罗磊的独立博客
S
Schneier on Security
腾讯CDC
Google DeepMind News
Google DeepMind News
小众软件
小众软件
量子位
N
News and Events Feed by Topic
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Hacker News: Ask HN
Hacker News: Ask HN
V
V2EX

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Rate Limiting Strategies in Go: Token Bucket, Leaky Bucket, and Sliding Window
Athreya aka · 2026-05-21 · via DEV Community

Hello, I'm Maneshwar. I'm building git-lrc, a Micro AI code reviewer that runs on every commit. It is free and source-available on Github. Star git-lrc to help devs discover the project. Do give it a try and share your feedback for improving the project.

Every backend service eventually meets a client that does not know when to stop. Sometimes it is a buggy retry loop, sometimes it is a scraper, sometimes it is your own well-meaning cron job firing a thousand requests in the same second.

The fix is the same: rate limiting.

In this post we will walk through the three algorithms you will encounter again and again — token bucket, leaky bucket, and sliding window — and how to actually use them in Go without writing them from scratch.

Go has solid, well-tested libraries for each, and unless you have a very good reason, you should use them.

We will keep this single-node.

Distributed rate limiting with Redis is its own rabbit hole, and I will save that for a follow-up post.

The three algorithms in one minute

Before touching code, here is the mental model you actually need.

Token bucket has a bucket that holds tokens.
Tokens are added at a steady rate up to some capacity.
Every request takes a token. No token, no request.
Because the bucket can fill up, it allows short bursts — useful when you want to be strict on average but tolerant of brief spikes.

Leaky bucket flips the perspective.
Requests go into a bucket; the bucket drains (leaks) at a steady rate.
If the bucket is full, new requests are dropped or made to wait.
It enforces a strict, smooth output rate. No bursts.

Sliding window counts requests inside a moving time window.
If you say "100 requests per minute," it really means "no more than 100 in any 60-second span."
It is the most accurate of the three but also the most expensive to compute precisely.

A quick comparison:

Algorithm Allows bursts? Output shape Typical use
Token bucket Yes Bursty within limits API endpoints, user quotas
Leaky bucket No Perfectly smooth Outbound calls to a strict upstream
Sliding window Configurable Accurate over time "N per minute" billing-style limits

Now let us actually use them.

1. Token bucket with golang.org/x/time/rate

The golang.org/x/time/rate package is the de facto standard for token bucket rate limiting in Go.

It is an official Go subrepository — same maintainers as the standard library, just versioned separately so it can evolve outside the standard library's compatibility lockstep.

You still have to go get it.

go get golang.org/x/time/rate

Enter fullscreen mode Exit fullscreen mode

The core type is rate.Limiter.

You create one with NewLimiter(r, b) where r is the refill rate (tokens per second) and b is the burst size (bucket capacity).

package main

import (
    "context"
    "fmt"
    "time"

    "golang.org/x/time/rate"
)

func main() {
    // 5 tokens per second, bucket holds up to 10
    limiter := rate.NewLimiter(5, 10)

    ctx := context.Background()
    for i := 0; i < 20; i++ {
        // Wait blocks until a token is available (or ctx is cancelled)
        if err := limiter.Wait(ctx); err != nil {
            fmt.Println("error:", err)
            return
        }
        fmt.Printf("request %d at %s\n", i, time.Now().Format("15:04:05.000"))
    }
}

Enter fullscreen mode Exit fullscreen mode

image

Limiter gives you three methods, and the difference between them is the whole point of the package:

  • Allow() — returns true if a token is available right now, otherwise false. Non-blocking. Use this when you want to drop excess requests (e.g. return 429 Too Many Requests).
  • Wait(ctx) — blocks until a token is available. Respects context cancellation. Use this for background workers that should slow down, not fail.
  • Reserve() — returns a Reservation telling you how long to wait. Use this when you want to make the decision yourself — for example, fail fast if the delay exceeds some threshold.

Drop-style: HTTP middleware with Allow()

The most common use case is "limit incoming HTTP requests and reject the overflow." Here is the middleware:

package main

import (
    "encoding/json"
    "net/http"

    "golang.org/x/time/rate"
)

func rateLimitMiddleware(next http.Handler) http.Handler {
    // 10 req/sec sustained, 20 burst
    limiter := rate.NewLimiter(10, 20)

    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !limiter.Allow() {
            w.Header().Set("Content-Type", "application/json")
            w.WriteHeader(http.StatusTooManyRequests)
            json.NewEncoder(w).Encode(map[string]string{
                "error": "rate limit exceeded, please retry later",
            })
            return
        }
        next.ServeHTTP(w, r)
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("hello\n"))
    })

    http.ListenAndServe(":8080", rateLimitMiddleware(mux))
}

Enter fullscreen mode Exit fullscreen mode

image

That is it. One limiter, shared across every request, dropping anything over 10/sec with a 20-request burst headroom.

Per-client limiting

A single global limiter is rarely what you want.

Usually you want each client (IP, API key, user ID) to have its own bucket so one noisy client cannot starve everyone else.

The pattern is a map of limiters keyed by client identifier.

package main

import (
    "net"
    "net/http"
    "sync"
    "time"

    "golang.org/x/time/rate"
)

type clientLimiter struct {
    limiter  *rate.Limiter
    lastSeen time.Time
}

type IPRateLimiter struct {
    clients map[string]*clientLimiter
    mu      sync.Mutex
    rate    rate.Limit
    burst   int
}

func NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter {
    rl := &IPRateLimiter{
        clients: make(map[string]*clientLimiter),
        rate:    r,
        burst:   b,
    }
    // Janitor: evict idle clients every minute
    go rl.cleanup()
    return rl
}

func (rl *IPRateLimiter) getLimiter(ip string) *rate.Limiter {
    rl.mu.Lock()
    defer rl.mu.Unlock()

    c, ok := rl.clients[ip]
    if !ok {
        lim := rate.NewLimiter(rl.rate, rl.burst)
        rl.clients[ip] = &clientLimiter{limiter: lim, lastSeen: time.Now()}
        return lim
    }
    c.lastSeen = time.Now()
    return c.limiter
}

func (rl *IPRateLimiter) cleanup() {
    for {
        time.Sleep(time.Minute)
        rl.mu.Lock()
        for ip, c := range rl.clients {
            if time.Since(c.lastSeen) > 3*time.Minute {
                delete(rl.clients, ip)
            }
        }
        rl.mu.Unlock()
    }
}

func (rl *IPRateLimiter) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // RemoteAddr is host:port; we want the host as the key
        host, _, err := net.SplitHostPort(r.RemoteAddr)
        if err != nil {
            host = r.RemoteAddr
        }
        if !rl.getLimiter(host).Allow() {
            http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
}

Enter fullscreen mode Exit fullscreen mode

image

A few things to note. First, the janitor goroutine: without it the map grows forever as new IPs keep showing up.

Second, r.RemoteAddr is host:port, so if you use it as the map key directly, the same client on a different ephemeral port gets a fresh bucket — which is not what you want. net.SplitHostPort fixes that.

Third, in any real deployment behind a load balancer or CDN, even the host portion of RemoteAddr is your proxy's IP — you need to extract the client IP from X-Forwarded-For or X-Real-IP (and verify the proxy is trusted, or you have just made spoofing trivial).

Wait-style: throttling outbound calls

When you are the client hitting some upstream API with a 100 req/sec ceiling, you want to slow down, not error out.

Wait is the right tool here.

func fetchAll(ctx context.Context, urls []string) {
    // ~100/sec, burst of 1 (no headroom beyond the steady rate)
    limiter := rate.NewLimiter(rate.Every(10*time.Millisecond), 1)

    var wg sync.WaitGroup
    for _, u := range urls {
        if err := limiter.Wait(ctx); err != nil {
            break // context cancelled
        }
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            fetch(u)
        }(u)
    }
    wg.Wait()
}

Enter fullscreen mode Exit fullscreen mode

image

Two things worth flagging in this snippet.

First, burst of 1 means a single token sits in the bucket, so the first call returns immediately and the rest are spaced ~10ms apart.

A burst of 0 would reject everything and Wait would block forever — 1 is the minimum useful value when you want "no real burst headroom."

Second, the Wait has to happen before the goroutine spawns, not inside it. If you put it inside, you launch a thousand goroutines in a microsecond and they all race through Wait together — you have throttled goroutine starts, but the actual HTTP calls all fire as soon as their tokens come in, which is not the same as throttling the calls.

Doing the wait in the calling loop and then spawning is what gives you actual rate-limited outbound traffic.

rate.Every(d) is a small helper that converts a desired interval into a rate.Limit. rate.Every(10*time.Millisecond) is the same as rate.Limit(100) but reads more naturally when you are thinking in terms of "one request every N milliseconds."

2. Leaky bucket with go.uber.org/ratelimit

Token bucket allows bursts.

Sometimes you do not want that.

If you are calling an upstream that absolutely cannot tolerate spikes — say, a partner API that throttles you on the millisecond, not the second — you want a leaky bucket: smooth, evenly-spaced output.

Uber's go.uber.org/ratelimit is the simplest leaky-bucket implementation in the Go ecosystem. The whole API is one method: Take().

go get go.uber.org/ratelimit

Enter fullscreen mode Exit fullscreen mode

package main

import (
    "fmt"
    "time"

    "go.uber.org/ratelimit"
)

func main() {
    rl := ratelimit.New(100) // 100 ops/sec, evenly spaced

    prev := time.Now()
    for i := 0; i < 10; i++ {
        now := rl.Take()
        fmt.Println(i, now.Sub(prev))
        prev = now
    }
}

Enter fullscreen mode Exit fullscreen mode

image

If you run this, every iteration after the first will print roughly 10ms.

The limiter does not give you 100 in a burst and then make you wait — it spaces them out exactly.

That is the leaky bucket guarantee.

Slack: a controlled amount of burstiness

Pure leaky bucket can be too strict.

If your producer is slightly bursty by nature, you may not want every single hiccup to cause queueing.

Uber's library has a "slack" knob for this.

With slack, the limiter can accumulate a small number of unspent requests during idle periods and let you burn through them in a burst later.

Worth being precise about what slack is, because it is not the same thing as a token bucket's burst capacity.

A token bucket refills continuously up to its capacity, so even a steady stream of requests can build up headroom if it briefly outpaces the consumer.

Slack only accumulates during idle time — if you are calling Take() continuously, slack does nothing.

It is a one-shot "you went quiet, so we will let you catch up" allowance, not an ongoing buffer.

// Default: allows up to 10 slack tokens (small built-in burst tolerance after idle)
rl := ratelimit.New(100)

// Strict mode: zero slack, perfectly even spacing
rl := ratelimit.New(100, ratelimit.WithoutSlack)

// Custom slack
rl := ratelimit.New(100, ratelimit.WithSlack(50))

Enter fullscreen mode Exit fullscreen mode

image

Note that WithoutSlack is a variable, not a function — no parentheses.

Per-minute and other windows

By default, New(n) means "n per second." If you want per-minute or per-hour, use Per:

rl := ratelimit.New(5, ratelimit.Per(time.Minute)) // 5 per minute

Enter fullscreen mode Exit fullscreen mode

When to pick Uber's library over x/time/rate

The honest answer is: only when you specifically need leaky-bucket semantics. x/time/rate can do almost everything Uber's library does and more (context support, Allow/Reserve semantics, dynamic rate changes).

But if your requirement is "evenly spaced output, no bursts," ratelimit.New(n) is one line and you are done.

One caveat: the library is stable and widely used, but not actively iterated on.

That is usually fine for something this small and well-defined — but if you want a library with more momentum behind it, x/time/rate is the safer pick.

3. Sliding window: when "N per minute" really means N

Token bucket and leaky bucket reason about rate.

They give you very good average-rate enforcement, but they do not give you an exact "no more than N requests in any rolling 60-second window" guarantee.

Sometimes that exact-count semantics is what you actually need — billing limits, login attempt windows, fairness across tenants.

The clean way to do this single-node is to keep a sorted slice of request timestamps per key, and on each request: evict timestamps older than the window, then check if the count is under the limit. Here is a self-contained implementation:

package ratelimit

import (
    "sort"
    "sync"
    "time"
)

type SlidingWindow struct {
    limit    int
    window   time.Duration
    requests map[string][]time.Time
    mu       sync.Mutex
}

func NewSlidingWindow(limit int, window time.Duration) *SlidingWindow {
    return &SlidingWindow{
        limit:    limit,
        window:   window,
        requests: make(map[string][]time.Time),
    }
}

func (sw *SlidingWindow) Allow(key string) bool {
    sw.mu.Lock()
    defer sw.mu.Unlock()

    now := time.Now()
    cutoff := now.Add(-sw.window)

    // Timestamps are appended in order, so we can binary-search the cutoff.
    timestamps := sw.requests[key]
    i := sort.Search(len(timestamps), func(i int) bool {
        return timestamps[i].After(cutoff)
    })
    timestamps = timestamps[i:]

    if len(timestamps) >= sw.limit {
        sw.requests[key] = timestamps
        return false
    }

    sw.requests[key] = append(timestamps, now)
    return true
}

Enter fullscreen mode Exit fullscreen mode

Usage:

sw := NewSlidingWindow(100, time.Minute) // 100 requests per rolling minute

if !sw.Allow("user:42") {
    // reject
}

Enter fullscreen mode Exit fullscreen mode

image

This is the "sliding log" variant — exact, but memory is O(limit × active_keys).

For high limits (say, 10k req/min across many keys), that becomes real memory.

The standard fix is the sliding window counter variant: keep two adjacent fixed windows (the current and previous minute), each with a single integer count, and estimate the rolling count as count_current + count_previous × (1 - elapsed_fraction_of_current_window).

You lose a bit of precision near window boundaries but drop from O(limit) per key to O(1).

For most real workloads, the simple log version above is fine; reach for the counter version when memory becomes a problem.

You also want a janitor here, same as the IP limiter, to evict keys nobody has hit in a while.

A word on distributed rate limiting

Everything above runs in a single process.

The moment you scale to two instances of your service behind a load balancer, your limits are effectively doubled — each instance has its own bucket.

For a single-instance side project this does not matter.

For anything serious, you need a shared store.

The standard answer is Redis.

The github.com/go-redis/redis_rate package implements GCRA (a leaky-bucket variant) on top of Redis with a single Lua script per check, which keeps it atomic and fast. Roughly:

limiter := redis_rate.NewLimiter(rdb)
res, _ := limiter.Allow(ctx, "user:42", redis_rate.PerSecond(10))
if res.Allowed == 0 {
    // reject
}

Enter fullscreen mode Exit fullscreen mode

I will cover the Redis side of this — including the Lua scripts, why GCRA wins over naive sliding windows at scale, and how to handle Redis going down — in a follow-up post.

For now, just know that when you outgrow single-node limiting, this is the next stop.

Which one should you use?

If you remember one thing from this post, remember this:

  • Default to golang.org/x/time/rate. It is well-tested, context-aware, and covers 90% of cases. Use Allow for HTTP servers, Wait for outbound clients.
  • Reach for go.uber.org/ratelimit when you specifically need evenly spaced output and no bursts.
  • Roll a sliding window when your requirement is genuinely "no more than N in any rolling window of duration W" and average-rate enforcement is not enough.

The biggest mistake I see is people writing their own token bucket from scratch and getting the math subtly wrong — off-by-one on the burst size, races on the refill, drift over long runs. The libraries exist. Use them.

Now go put a 429 in front of whatever is currently melting your server.

AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs -- without telling you. You often find out in production.

git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.*

Any feedback or contributors are welcome! It's online, source-available, and ready for anyone to use.

⭐ Star it on GitHub:


AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs -- without telling you. You often find out in production.

git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.

See It In Action

See git-lrc catch serious security issues such as leaked credentials, expensive cloud operations, and sensitive material in log statements

git-lrc-intro-60s.mp4

Why

  • 🤖 AI agents silently break things. Code removed. Logic changed. Edge cases gone. You won't notice until production.
  • 🔍 Catch it before it ships. AI-powered inline comments show you exactly what changed and what looks wrong.
  • 🔁 Build a