Rate Limiting and Throttling in Go: A Complete Guide with Examples

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Rate Limiting and Throttling in Go: How to Protect Your API and Stay Sane

\n\n

Imagine: you've written a perfect API in Go. It's lightning fast, processing requests in milliseconds, and then — bam! — a flood of requests hits the server. Maybe it's a bot, maybe a client bug, or maybe just hype around your product. Without protection, the server goes down, users leave, and you're stuck debugging logs until morning.

\n\n

Sound familiar? Then rate limiting and throttling are your new best friends. In this article on pythonlib.ru, we'll break down how to implement them in Go — a language built for high-load systems. No fluff, just live code and real-world cases.

\n\n

Rate Limiting vs Throttling: What's the Difference?

\n\n

First, let's clarify the terms. Many people confuse these concepts, but there is a difference.

\n\n
    \n
  • Rate Limiting — a strict limit: "No more than 100 requests per minute." If the limit is exceeded, the server responds with 429 Too Many Requests. Fair and strict.
  • \n
  • Throttling — a soft regulation: "We'll slow you down, but won't block you." For example, instead of responding to all requests immediately, we queue them and process them at a fixed rate.
  • \n
\n\n

In Go, both approaches are elegantly implemented thanks to goroutines and channels. Let's look at specific examples.

\n\n

Simple Rate Limiter with Channels

\n\n

The most basic way to limit speed is to use a ticker from the time package. It's like a metronome: each "tick" allows one request.

\n\n
package main\n\nimport (\n    "fmt"\n    "time"\n)\n\nfunc main() {\n    // Create a channel that will send tokens every 200 ms\n    limiter := time.Tick(200 * time.Millisecond)\n\n    // Simulate 10 requests\n    for i := 1; i <= 10; i++ {\n        <-limiter // Wait for permission\n        fmt.Printf("Request %d processed at %s\\n", i, time.Now().Format("15:04:05.000"))\n    }\n}
\n\n

What's happening here? time.Tick creates a channel that sends a value every 200 milliseconds. Our goroutine blocks on <-limiter until it receives a token. The result is exactly 5 requests per second, no exceptions.

\n\n

The downside: if there are fewer requests than ticks, we just wait. Inefficient? Yes. But for simple scenarios, it's perfect.

\n\n

Burst Limiting with a Buffer

\n\n

What if we need to allow traffic "bursts"? For example, allow 10 requests instantly, then limit to 5 per second. For this, we use a buffered channel — the classic Token Bucket.

\n\n
package main\n\nimport (\n    "fmt"\n    "time"\n)\n\nfunc main() {\n    // Buffered channel with 5 tokens\n    tokens := make(chan struct{}, 5)\n\n    // Filler goroutine: adds a token every 200 ms\n    go func() {\n        ticker := time.NewTicker(200 * time.Millisecond)\n        defer ticker.Stop()\n        for range ticker.C {\n            select {\n            case tokens <- struct{}{}:\n            default:\n                // Channel is full — do nothing\n            }\n        }\n    }()\n\n    // Simulate 15 requests\n    for i := 1; i <= 15; i++ {\n        <-tokens // Take a token (wait if empty)\n        fmt.Printf("Request %d processed at %s\\n", i, time.Now().Format("15:04:05.000"))\n        time.Sleep(50 * time.Millisecond) // Small delay between requests\n    }\n}
\n\n

The first 5 requests will go through instantly (buffer is full), then one every 200 ms. This pattern is perfect for APIs where a client might suddenly start sending many requests (e.g., after a restart).

\n\n

Throttling with WaitGroup and Worker Pool

\n\n

Now let's move on to throttling. Suppose we have a task queue (e.g., sending an email newsletter). We don't want to block the client, but we also don't want to overload the

Blogs

Book Recommendations