Skip to main content

Echo Rate Limiting

Rate limiting is an essential technique for protecting your API from abuse, preventing service overloads, and ensuring fair usage among clients. In this tutorial, we'll explore how to implement rate limiting in your Echo framework applications.

What is Rate Limiting?

Rate limiting restricts how many requests a user or client can make to your API within a specific time period. When the limit is exceeded, the server typically responds with a 429 Too Many Requests status code.

Common rate limiting strategies include:

  • Fixed window: Count requests in fixed time intervals (e.g., 100 requests per minute)
  • Sliding window: Track requests across a continuously moving time window
  • Token bucket: Use tokens that refill at a fixed rate to permit requests
  • Leaky bucket: Process requests at a constant rate, queuing or dropping excess requests

Why Implement Rate Limiting?

Rate limiting provides several benefits:

  1. Protection from abuse - Prevents malicious attacks like DDoS
  2. Resource management - Ensures fair distribution of server resources
  3. Cost control - Helps manage costs for APIs that depend on usage-based services
  4. Performance optimization - Maintains consistent performance during traffic spikes

Basic Rate Limiting in Echo

Echo doesn't include built-in rate limiting, but we can use third-party middleware. Let's start with a simple example using the popular golang.org/x/time/rate package.

First, install the required package:

bash
go get -u golang.org/x/time/rate

Now, let's implement a basic rate limiter middleware:

go
package middleware

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

"github.com/labstack/echo/v4"
"golang.org/x/time/rate"
)

// IPRateLimit creates a rate limiting middleware using client IP addresses
func IPRateLimit(rps int, burst int) echo.MiddlewareFunc {
// Create a map to store rate limiters for each IP
limiters := make(map[string]*rate.Limiter)
mtx := &sync.Mutex{}

return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get IP from request
ip := c.RealIP()

// Lock the mutex to ensure concurrent access to the map
mtx.Lock()

// Check if the IP already has a rate limiter
limiter, exists := limiters[ip]
if !exists {
// Create a new rate limiter for this IP
limiter = rate.NewLimiter(rate.Limit(rps), burst)
limiters[ip] = limiter
}

// Unlock the mutex
mtx.Unlock()

// Check if this request exceeds the rate limit
if !limiter.Allow() {
return c.JSON(http.StatusTooManyRequests, map[string]string{
"error": "Rate limit exceeded. Try again later.",
})
}

// Continue to the next middleware/handler
return next(c)
}
}
}

Using the Rate Limit Middleware

Here's how to integrate the rate limiter in your Echo application:

go
package main

import (
"net/http"

"github.com/labstack/echo/v4"
"yourproject/middleware"
)

func main() {
e := echo.New()

// Apply rate limiting middleware - 5 requests per second with a burst of 10
e.Use(middleware.IPRateLimit(5, 10))

// Define routes
e.GET("/api/data", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "Access successful",
})
})

e.Logger.Fatal(e.Start(":8080"))
}

Testing the Rate Limiter

When you run the application and make requests:

  • The first 10 requests will be allowed immediately (due to the burst parameter)
  • After that, requests will be limited to 5 per second
  • Exceeding these limits will result in a 429 status code response

Advanced Rate Limiting Techniques

Per-User Rate Limiting

In real-world applications, you might want to limit based on user accounts rather than IP addresses:

go
// UserRateLimit creates a rate limiting middleware using user IDs
func UserRateLimit(rps int, burst int) echo.MiddlewareFunc {
limiters := make(map[string]*rate.Limiter)
mtx := &sync.Mutex{}

return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get user ID from your authentication system
// This is just an example - replace with your actual user ID retrieval
userID := c.Get("user_id").(string)
if userID == "" {
// For unauthenticated users, fall back to IP
userID = "ip:" + c.RealIP()
}

mtx.Lock()
limiter, exists := limiters[userID]
if !exists {
limiter = rate.NewLimiter(rate.Limit(rps), burst)
limiters[userID] = limiter
}
mtx.Unlock()

if !limiter.Allow() {
return c.JSON(http.StatusTooManyRequests, map[string]string{
"error": "Rate limit exceeded. Try again later.",
})
}

return next(c)
}
}
}

Tiered Rate Limiting

For applications with different user tiers or subscription levels:

go
// TieredRateLimit applies different rate limits based on user tiers
func TieredRateLimit() echo.MiddlewareFunc {
limiters := make(map[string]*rate.Limiter)
mtx := &sync.Mutex{}

// Define tier limits (requests per second)
tierLimits := map[string]int{
"free": 2,
"basic": 5,
"premium": 20,
"enterprise": 50,
}

// Define tier bursts
tierBursts := map[string]int{
"free": 5,
"basic": 10,
"premium": 30,
"enterprise": 100,
}

return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get user ID and tier from your auth system
userID := c.Get("user_id").(string)
userTier := c.Get("user_tier").(string)

// Default to free tier if not specified
if userTier == "" {
userTier = "free"
}

// Create a unique key for this user
key := userID + ":" + userTier

mtx.Lock()
limiter, exists := limiters[key]
if !exists {
// Get the appropriate limits for this tier
rps := tierLimits[userTier]
burst := tierBursts[userTier]
limiter = rate.NewLimiter(rate.Limit(rps), burst)
limiters[key] = limiter
}
mtx.Unlock()

if !limiter.Allow() {
return c.JSON(http.StatusTooManyRequests, map[string]string{
"error": "Rate limit exceeded. Try again later.",
})
}

return next(c)
}
}
}

Distributed Rate Limiting

For applications running on multiple servers, you'll need a distributed solution. Let's use Redis for this purpose:

First, install the Redis client:

bash
go get -u github.com/go-redis/redis/v8

Then implement the middleware:

go
package middleware

import (
"context"
"net/http"
"strconv"
"time"

"github.com/go-redis/redis/v8"
"github.com/labstack/echo/v4"
)

// RedisRateLimiter creates a distributed rate limiter using Redis
func RedisRateLimiter(rps int, windowSize time.Duration) echo.MiddlewareFunc {
// Setup Redis client
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Use your Redis address
})
ctx := context.Background()

return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Create a key based on IP or user ID
key := "ratelimit:" + c.RealIP()

// Current timestamp in seconds
now := time.Now().Unix()

// Window start time (current time - window size)
windowStart := now - int64(windowSize.Seconds())

// Multi-command transaction
pipe := rdb.Pipeline()

// Remove counts older than the window
pipe.ZRemRangeByScore(ctx, key, "0", strconv.FormatInt(windowStart, 10))

// Add current request with timestamp as score
pipe.ZAdd(ctx, key, &redis.Z{Score: float64(now), Member: strconv.FormatInt(now, 10)})

// Set expiry on the key to ensure cleanup
pipe.Expire(ctx, key, windowSize)

// Count requests in the current window
countCmd := pipe.ZCard(ctx, key)

// Execute pipeline
_, err := pipe.Exec(ctx)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Rate limiting error",
})
}

// Check if count exceeds the limit
count := countCmd.Val()
if count > int64(rps) {
return c.JSON(http.StatusTooManyRequests, map[string]string{
"error": "Rate limit exceeded. Try again later.",
"limit": strconv.Itoa(rps),
"remaining": "0",
"reset": strconv.FormatInt(now + int64(windowSize.Seconds()), 10),
})
}

// Add rate limit headers to response
c.Response().Header().Set("X-RateLimit-Limit", strconv.Itoa(rps))
c.Response().Header().Set("X-RateLimit-Remaining", strconv.FormatInt(int64(rps)-count, 10))
c.Response().Header().Set("X-RateLimit-Reset", strconv.FormatInt(now+int64(windowSize.Seconds()), 10))

return next(c)
}
}
}

Implementing Rate Limiting Best Practices

Informative Responses

When rate limiting a client, provide helpful information in the response:

go
if !limiter.Allow() {
c.Response().Header().Set("Retry-After", "60")
c.Response().Header().Set("X-RateLimit-Limit", strconv.Itoa(rps))
c.Response().Header().Set("X-RateLimit-Remaining", "0")
c.Response().Header().Set("X-RateLimit-Reset", strconv.FormatInt(time.Now().Add(time.Minute).Unix(), 10))

return c.JSON(http.StatusTooManyRequests, map[string]string{
"error": "Rate limit exceeded",
"message": "Please try again after 60 seconds",
})
}

Gradual Degradation

Instead of completely blocking users, consider gradually degrading service:

go
func GradualRateLimit(rps int, burst int) echo.MiddlewareFunc {
limiters := make(map[string]*rate.Limiter)
mtx := &sync.Mutex{}

return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ip := c.RealIP()

mtx.Lock()
limiter, exists := limiters[ip]
if !exists {
limiter = rate.NewLimiter(rate.Limit(rps), burst)
limiters[ip] = limiter
}
mtx.Unlock()

// Get reservation instead of just Allow/Reject
reservation := limiter.Reserve()

if !reservation.OK() {
// Rate limit far exceeded - reject completely
return c.JSON(http.StatusTooManyRequests, map[string]string{
"error": "Rate limit exceeded. Try again later.",
})
}

// Calculate the delay needed
delay := reservation.Delay()

if delay > 0 {
// If delay is reasonable (less than 2 seconds), wait before processing
if delay < 2*time.Second {
time.Sleep(delay)
return next(c)
} else {
// If delay is too long, cancel reservation and reject request
reservation.Cancel()
return c.JSON(http.StatusTooManyRequests, map[string]string{
"error": "Rate limit exceeded. Try again later.",
"retry_after": strconv.FormatFloat(delay.Seconds(), 'f', 0, 64),
})
}
}

// No delay needed, proceed normally
return next(c)
}
}
}

Real-world Example: API Gateway with Multiple Rate Limits

Here's a comprehensive example of an API gateway with different rate limits for various endpoints:

go
package main

import (
"net/http"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"yourproject/ratelimit"
)

func main() {
e := echo.New()

// Logger middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())

// Public endpoints - strict rate limiting
public := e.Group("/api/public")
public.Use(ratelimit.IPRateLimit(2, 5)) // 2 rps, burst of 5

public.GET("/info", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"status": "OK",
"version": "1.0.0",
})
})

// User endpoints - authentication required with user-based rate limiting
auth := e.Group("/api/user")
auth.Use(authMiddleware) // Your authentication middleware
auth.Use(ratelimit.UserRateLimit(10, 20)) // 10 rps, burst of 20

auth.GET("/profile", func(c echo.Context) error {
userID := c.Get("user_id").(string)
return c.JSON(http.StatusOK, map[string]string{
"user_id": userID,
"message": "Profile accessed successfully",
})
})

// Premium endpoints - higher limits for paying customers
premium := e.Group("/api/premium")
premium.Use(authMiddleware)
premium.Use(ratelimit.TieredRateLimit())

premium.GET("/data", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "Premium data accessed",
})
})

// Admin endpoints - very high limits
admin := e.Group("/api/admin")
admin.Use(authMiddleware)
admin.Use(adminMiddleware) // Check admin privileges
admin.Use(ratelimit.IPRateLimit(100, 200)) // 100 rps, burst of 200

admin.GET("/stats", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "Admin stats accessed",
})
})

e.Logger.Fatal(e.Start(":8080"))
}

// Sample authentication middleware (simplified)
func authMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// In a real app, you would validate tokens, etc.
token := c.Request().Header.Get("Authorization")
if token == "" {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Missing authorization token",
})
}

// Simulate retrieving user ID from token
c.Set("user_id", "user_123")
c.Set("user_tier", "premium")

return next(c)
}
}

// Sample admin middleware (simplified)
func adminMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
userID := c.Get("user_id").(string)
// In a real app, you would check if user has admin privileges
if userID != "admin_user" {
return c.JSON(http.StatusForbidden, map[string]string{
"error": "Admin privileges required",
})
}

return next(c)
}
}

Summary

Rate limiting is a crucial aspect of API design that helps protect your services from abuse while ensuring fair access for all clients. In this tutorial, we've explored:

  • Basic concepts of rate limiting and why it's important
  • Implementing simple IP-based rate limiting in Echo
  • Advanced techniques like user-based and tiered rate limiting
  • Distributed rate limiting with Redis for multi-server deployments
  • Best practices for implementing user-friendly rate limiting

By implementing appropriate rate limiting strategies, you can create more robust, secure, and reliable APIs that perform well even under heavy load or when faced with potential abuse.

Additional Resources

Exercises

  1. Implement a rate limiter that allows different limits based on the HTTP method (e.g., higher limits for GET, lower for POST/PUT)
  2. Create a custom sliding window rate limiter without using any external libraries
  3. Extend the Redis-based rate limiter to support different windows (e.g., per-second, per-minute, and per-hour limits simultaneously)
  4. Build a system that logs and alerts when clients repeatedly hit rate limits, which might indicate abuse attempts
  5. Implement a rate limiter that learns from traffic patterns and automatically adjusts limits during peak usage times


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)