Tollbooth
This is a generic middleware to rate-limit HTTP requests.
NOTE 1: This library is considered finished.
NOTE 2: Major version changes are backward-incompatible. v2.0.0
streamlines the ugliness of the old API.
Versions
v1.0.0: This version maintains the old API but all the thirdparty modules are moved to their own repo.
v2.x.x: Brand-new API for the sake of code cleanup, thread safety, & auto-expiring data structures.
v3.x.x: Apparently we have been using golang.org/x/time/rate incorrectly. See issue #48. It always limits X number per 1 second. The time duration is not changeable, so it does not make sense to pass TTL to tollbooth.
v4.x.x: Float64 for max requests per second
v5.x.x: go.mod and go.sum
v6.x.x: Replaced go-cache
with github.com/go-pkgz/expirable-cache
because go-cache
leaks goroutines.
v7.x.x: Replaced time/rate
with embedded time/rate
so that we can support more rate limit headers.
Five Minute Tutorial
package main
import (
"net/http"
"github.com/didip/tollbooth/v7"
)
func HelloHandler(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Hello, World!"))
}
func main() {
http.Handle("/", tollbooth.LimitFuncHandler(tollbooth.NewLimiter(1, nil), HelloHandler))
http.ListenAndServe(":12345", nil)
}
Features
-
Rate-limit by request's remote IP, path, methods, custom headers, & basic auth usernames.
import (
"time"
"github.com/didip/tollbooth/v7"
"github.com/didip/tollbooth/v7/limiter"
)
lmt := tollbooth.NewLimiter(1, nil)
lmt = tollbooth.NewLimiter(1, &limiter.ExpirableOptions{DefaultExpirationTTL: time.Hour})
lmt.SetIPLookups([]string{"RemoteAddr", "X-Forwarded-For", "X-Real-IP"})
lmt.SetMethods([]string{"GET", "POST"})
lmt.SetBasicAuthUsers([]string{"bob", "jane", "didip", "vip"})
lmt.RemoveBasicAuthUsers([]string{"vip"})
lmt.SetHeader("X-Access-Token", []string{"abc123", "xyz098"})
lmt.RemoveHeader("X-Access-Token")
lmt.RemoveHeaderEntries("X-Access-Token", []string{"limitless-token"})
lmt.SetIPLookups([]string{"RemoteAddr", "X-Forwarded-For", "X-Real-IP"}).
SetMethods([]string{"GET", "POST"}).
SetBasicAuthUsers([]string{"sansa"}).
SetBasicAuthUsers([]string{"tyrion"})
-
Compose your own middleware by using LimitByKeys()
.
-
Header entries and basic auth users can expire over time (to conserve memory).
import "time"
lmt := tollbooth.NewLimiter(1, nil)
lmt.SetTokenBucketExpirationTTL(time.Hour)
lmt.SetBasicAuthExpirationTTL(time.Hour)
lmt.SetHeaderEntryExpirationTTL(time.Hour)
-
Upon rejection, the following HTTP response headers are available to users:
-
X-Rate-Limit-Limit
The maximum request limit.
-
X-Rate-Limit-Duration
The rate-limiter duration.
-
X-Rate-Limit-Request-Forwarded-For
The rejected request X-Forwarded-For
.
-
X-Rate-Limit-Request-Remote-Addr
The rejected request RemoteAddr
.
Upon both success and rejection RateLimit headers are sent:
-
RateLimit-Limit
The maximum request limit within the time window (1s).
-
RateLimit-Reset
The rate-limiter time window duration in seconds (always 1s).
-
RateLimit-Remaining
The remaining tokens.
-
Customize your own message or function when limit is reached.
lmt := tollbooth.NewLimiter(1, nil)
lmt.SetMessage("You have reached maximum request limit.")
lmt.SetMessageContentType("text/plain; charset=utf-8")
lmt.SetOnLimitReached(func(w http.ResponseWriter, r *http.Request) { fmt.Println("A request was rejected") })
-
Tollbooth does not require external storage since it uses an algorithm called Token Bucket (Go library: golang.org/x/time/rate).
Other Web Frameworks
Sometimes, other frameworks require a little bit of shim to use Tollbooth. These shims below are contributed by the community, so I make no promises on how well they work. The one I am familiar with are: Chi, Gin, and Negroni.
My other Go libraries
-
ErrStack: A small library to combine errors and also display filename and line number.
-
Stopwatch: A small library to measure latency of things. Useful if you want to report latency data to Graphite.
-
LaborUnion: A dynamic worker pool library.
-
Gomet: Simple HTTP client & server long poll library for Go. Useful for receiving live updates without needing Websocket.
Contributions
Before sending a PR with code changes, please make sure altered code is covered with tests which are passing, and that golangci-lint shows no errors.
To check the linter output, install it and then run golangci-lint run
in the root directory of the repository.