Implementing rate limiters in the Echo framework

October 8, 2022

In this article I will show you how to rate limit requests in the Echo framework1, in combination with the package golimiter2.

go get github.com/mrwaggel/golimiter

This can be useful if you wish to limit the amount of times an user can submit a form, taking count how often a captcha3 was entered incorrectly, or to limit requests to your API4, in general to reduce stress caused by spam on your web server.

Example for a single route

In this example we will limit the amount of times a user can access the index page of your Echo server. Scroll or jump down for the middleware example.

package main
import (
	"fmt"
	"github.com/labstack/echo/v4"
	"github.com/mrwaggel/golimiter"
	"net/http"
	"time"
)
const limitPerMinute = 5
var indexLimiter = golimiter.New(limitPerMinute, time.Minute)
func main() {
	e := echo.New()
	e.GET("", index)
	err := e.Start(":8181")
	if err != nil {
		// ...
	}
}
func index(c echo.Context) error {
	// Get the IP address from the requester
	IP := c.RealIP()
	// Check if the given IP is rate limited
	if indexLimiter.IsLimited(IP) {
		return c.String(http.StatusTooManyRequests, fmt.Sprintf("429: Too many request from %s", IP))
	}
	// Add a request to the count for the Ip
	indexLimiter.Increment(IP)
	totalRequestPastMinute := indexLimiter.Count(IP)
	totalRemaining := limitPerMinute - totalRequestPastMinute
	return c.String(http.StatusOK, fmt.Sprintf(""+
		"Your IP %s is not rate limited!\n"+
		"You made %d requests in the last minute.\n"+
		"You are allowed to make %d more request.\n"+
		"Maximum request you can make per minute is %d.",
		IP, totalRequestPastMinute, totalRemaining, limitPerMinute))
}

We declare a limiter that states a given key can only reach a limit 5 per minute. In this case the key is the IP address from which the request comes from.

As long as the user who requests the index page is within their limit, it will show the following output.

Your IP ::1 is not rate limited!
You made 2 requests in the last minute.
You are allowed to make 3 more request.
Maximum request you can make per minute is 5.

However once the user reached and exceeded the limit they will be shown the following message, with the correct HTTP status code 429 Too many requests5.

429: Too many request from ::1

Why use of (echo.Context).RealIP() ?

You could indeed use the RemoteAddr6 field from the (echo.Context).Request, but this value will be incorrect when your web server is running behind a proxy.

// RealIP returns the client's network address based on `X-Forwarded-For`
// or `X-Real-IP` request header.

Do note that if your proxy does not follow the conventional rules like the headers described above you will be returned the IP address of your proxy, in that case see the IPExtractor type7.

As a middleware

If you wish to limit all requests on any page, you can use a middleware8 like this.

package main
import (
	"github.com/labstack/echo/v4"
	"github.com/mrwaggel/golimiter"
	"net/http"
	"time"
)
const limitPerMinute = 5
var limiter = golimiter.New(limitPerMinute, time.Minute)
func RateLimitMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		// Check if the IP is limited
		if limiter.IsLimited(c.RealIP()) {
			return c.String(http.StatusTooManyRequests, "429: Too many requests")
		}
		// Increment the value for the IP
		limiter.Increment(c.RealIP())
		// Continue default operation of Echo
		return next(c)
	}
}
func main() {
	e := echo.New()
	e.Use(RateLimitMiddleware)
	e.GET("", index)
	err := e.Start(":8181")
	if err != nil {
		// ...
	}
}
func index(c echo.Context) error {
	return c.String(http.StatusOK, "200: Status OK!")
}

For a specific group

If you, for example, wish to limit request inside the URI9 "/api" group, define the use of the middleware like this, and now the limiter is only contained to this group.

e := echo.New()
apiGroup := e.Group("/api", RateLimitMiddleware)
apiGroup.GET(...)
apiGroup.POST(...)

Read also

Golang serving resume able file downloads with net/http
How to Create Linked Lists in Go
Embedding Files into Your Go Program
Implementing a TCP Client and Server in Go: A Ping Pong Example
Discovering Go: A Deep Dive into Google's Own Programming Language
Go random uint8 and uint16
Comments
Noctiluca
12/06/2022 18:38

Super useful information 😊 Thank you for sharing it!

permalink
References
3
CAPTCHA - Wikipedia

https://en.wikipedia.org/wiki/CAPTCHA

cached copy
4
API - Wikipedia

https://en.wikipedia.org/wiki/API

cached copy
5
429 Too Many Requests - HTTP | MDN

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429#:~:text=The%20HTTP%20429%20Too%20Many,before%20making%20a%20new%20request.

cached copy
6
go/request.go at master · golang/go · GitHub

https://github.com/golang/go/blob/master/src/net/http/request.go#L286

cached copy
7
echo package - github.com/labstack/echo/v4 - Go Packages

https://pkg.go.dev/github.com/labstack/echo/v4#IPExtractor

cached copy
9
Uniform Resource Identifier - Wikipedia

https://en.wikipedia.org/wiki/Uniform_Resource_Identifier

cached copy
Tags