Simple middleware to rate-limit HTTP requests.

GoDoc license

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.

Five Minute Tutorial

package main

import (
    "github.com/didip/tollbooth"
    "net/http"
)

func HelloHandler(w http.ResponseWriter, req *http.Request) {
    w.Write([]byte("Hello, World!"))
}

func main() {
    // Create a request limiter per handler.
    http.Handle("/", tollbooth.LimitFuncHandler(tollbooth.NewLimiter(1, nil), HelloHandler))
    http.ListenAndServe(":12345", nil)
}

Features

  1. Rate-limit by request's remote IP, path, methods, custom headers, & basic auth usernames.

    import (
        "time"
        "github.com/didip/tollbooth/limiter"
    )
    
    lmt := tollbooth.NewLimiter(1, nil)
    
    // or create a limiter with expirable token buckets
    // This setting means:
    // create a 1 request/second limiter and
    // every token bucket in it will expire 1 hour after it was initially set.
    lmt = tollbooth.NewLimiter(1, &limiter.ExpirableOptions{DefaultExpirationTTL: time.Hour})
    
    // Configure list of places to look for IP address.
    // By default it's: "RemoteAddr", "X-Forwarded-For", "X-Real-IP"
    // If your application is behind a proxy, set "X-Forwarded-For" first.
    lmt.SetIPLookups([]string{"RemoteAddr", "X-Forwarded-For", "X-Real-IP"})
    
    // Limit only GET and POST requests.
    lmt.SetMethods([]string{"GET", "POST"})
    
    // Limit based on basic auth usernames.
    // You add them on-load, or later as you handle requests.
    lmt.SetBasicAuthUsers([]string{"bob", "jane", "didip", "vip"})
    // You can remove them later as well.
    lmt.RemoveBasicAuthUsers([]string{"vip"})
    
    // Limit request headers containing certain values.
    // You add them on-load, or later as you handle requests.
    lmt.SetHeader("X-Access-Token", []string{"abc123", "xyz098"})
    // You can remove all entries at once.
    lmt.RemoveHeader("X-Access-Token")
    // Or remove specific ones.
    lmt.RemoveHeaderEntries("X-Access-Token", []string{"limitless-token"})
    
    // By the way, the setters are chainable. Example:
    lmt.SetIPLookups([]string{"RemoteAddr", "X-Forwarded-For", "X-Real-IP"}).
        SetMethods([]string{"GET", "POST"}).
        SetBasicAuthUsers([]string{"sansa"}).
        SetBasicAuthUsers([]string{"tyrion"})
  2. Compose your own middleware by using LimitByKeys().

  3. Header entries and basic auth users can expire over time (to conserve memory).

    import "time"
    
    lmt := tollbooth.NewLimiter(1, nil)
    
    // Set a custom expiration TTL for token bucket.
    lmt.SetTokenBucketExpirationTTL(time.Hour)
    
    // Set a custom expiration TTL for basic auth users.
    lmt.SetBasicAuthExpirationTTL(time.Hour)
    
    // Set a custom expiration TTL for header entries.
    lmt.SetHeaderEntryExpirationTTL(time.Hour)
  4. 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.

  5. Customize your own message or function when limit is reached.

    lmt := tollbooth.NewLimiter(1, nil)
    
    // Set a custom message.
    lmt.SetMessage("You have reached maximum request limit.")
    
    // Set a custom content-type.
    lmt.SetMessageContentType("text/plain; charset=utf-8")
    
    // Set a custom function for rejection.
    lmt.SetOnLimitReached(func(w http.ResponseWriter, r *http.Request) { fmt.Println("A request was rejected") })
  6. 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

  • 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.

Comments
  • limitReachedWithTokenBucketTTL probably useless with x/time/rate burst == 1

    limitReachedWithTokenBucketTTL probably useless with x/time/rate burst == 1

    since #52 the private function func (l *Limiter) limitReachedWithTokenBucketTTL(key string, tokenBucketTTL time.Duration) bool and thus the tokenBucketTTL functionality doesn't make sense anymore: The burst parameter b in x/time/rate NewLimiter is used as max burst and initial bucket size (see comment in https://godoc.org/golang.org/x/time/rate#Limiter and https://github.com/golang/time/blob/master/rate/rate.go#L346). So a first request to a new (re)created bucket is always granted. Because of the initial bucket size of one, a second request is only granted, if the time elapsed (d) fullfills 1/d < r with r being the bucket refill rate - in tollbooth case rate.Limit(lmtMax).

    So a bucket ttl only slighly increases the max rate, which is neglectable for 1/ttl << r.

    Does it make sense to introduce an initial bucket size to the ExpirableOptions struct and use it if the ttl is not set to the default value? I'll happily prepare a PR if all of the above makes sense.

  • Embed time/rate, set RateLimit headers

    Embed time/rate, set RateLimit headers

    Will resolve #88 once done. I went the lazy way of embedding time/rate instead of rewriting it, function we need will be added in https://github.com/golang/go/issues/50035 in the future.

    ToDo:

    • [x] make countLimitWithTokenBucketTTL work
    • [x] adjust Readme
    • [x] add tests
    • [x] manual tests
  • Remove TTL. Use max as bucket refill rate.

    Remove TTL. Use max as bucket refill rate.

    Fix #48

    This change removes the TTL, as it doesn't necessarily make sense for use with the token bucket. The token bucket refills at a rate of r tokens per second, which is defined by the max value in tollbooth. As such, it makes sense that tollbooth only supports "number of requests per second", rather than allowing the user to define a duration for those requests.

    This is a breaking change and existing code will need to be updated. As such, you may wish to update a major version in the semver, or do it as a bugfix, as the previous implementation was broken and does not function as described/intended.

  • Incorrect handling of IPv6 addresses in some configurations

    Incorrect handling of IPv6 addresses in some configurations

    Hi, I noticed that this library (along with others) assumes that the port number is always provided in http.Request.RemoteAddr

    ... Because this is an incorrect assumption, this breaks IPv6 on such installations where the port number is NOT provided: https://play.golang.org/p/oFZhZ6BCf3

    Code to demonstrate issue

    package main
    
    import (
    	"fmt"
    	"strings"
    )
    
    func ipAddrFromRemoteAddr(s string) string {
    	idx := strings.LastIndex(s, ":")
    	if idx == -1 {
    		return s
    	}
    	return s[:idx]
    }
    
    func main() {
    	fmt.Println("OK:", ipAddrFromRemoteAddr("127.0.0.1"))
    	fmt.Println("MISSING FINAL BLOCK:", ipAddrFromRemoteAddr("fe80:0000:0000:0000:34cb:9850:4868:9d2c"))
    	fmt.Println("MISSING FINAL BLOCK:", ipAddrFromRemoteAddr("fe80::34cb:9850:4868:9d2c"))
    
    }
    

    Result:

    OK: 127.0.0.1
    MISSING FINAL BLOCK: fe80:0000:0000:0000:34cb:9850:4868
    MISSING FINAL BLOCK: fe80::34cb:9850:4868
    

    It's probably necessary to validate the IPv6 address first in order to detect if the port is not there. There is actually a pretty bad bug since a lot of App Engine deployments might be using this as-is and Google has no intention of changing this "portless" behavior.

  • Switch to expirable-cache

    Switch to expirable-cache

    Resolves #84. I haven't found a cache that wouldn't spawn goroutines and would have the unlimited size so I published one similar to the one I contributed to hashicorp/golang-lru in https://github.com/hashicorp/golang-lru/pull/68 as a separate project and switched tollbooth to it.

    tollbooth API stays exactly the same, the only API difference is ExpirableOptions.ExpireJobInterval is unused becomes and becomes deprecated.

    The big difference with previously used go-cache is the following: expired entries are deleted only on new Set calls on the same cache.

    The only alternative to preserve exact same behavior as previously I could think of is calling DeleteExpired in this package and giving users the ability to terminate these goroutines using Close() call on a Limiter, which is kind of ugly but would do the trick. However, creating Close() function would allow us to switch to a number of different caches that provide their own Close() functions and then just call them inside our Close.

    Please let me know what you think about that.

  • juju/ratelimit is LGPL

    juju/ratelimit is LGPL

    Firstly, great middleware, but I'm wondering if there are implications with the underlying implementations violating section 4 of the LGPL? Your library is MIT, but the juju implementation is LGPL and because go is statically linked we appear to be in violation for commercially shipped rather than open source projects.

    IANAL but I was wondering if you've had any thoughts on this. Thanks.

  • Limiter doesn't behave as documentation describes

    Limiter doesn't behave as documentation describes

    Greetings!

    I am seeing some odd behavior with how the limiter limits requests.

    We are constructing a limiter like this:

    limiter := tollbooth.NewLimiter(20, time.Second, nil)
    

    My understanding of this, based on the documentation and code, is that the limiter will now allow a maximum of 20 requests per second, per path, per IP. If a client attempts to exceed 20 req/sec/path/IP, it will receive a 429 response.

    However, this is not what we are seeing. We have a client that hits the same path about 2 times per second. After about six minutes at 2 req/sec, tollbooth starts responding with 429 errors here and there.

    Either my understanding of how it is supposed to work is incorrect, or my configuration is incorrect, but it seems that setting 20/time.Second should never cause a 429 response under a load of 2/req/sec/path/IP.

    Is it possible there is a bug somewhere in this package?

    Please let me know what I'm missing here. Thanks!

  • Configurable option for using ratelimit standardization proposal

    Configurable option for using ratelimit standardization proposal

    I wish

    to be able to support via configuration the following standardization proposal: https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers

    gh repo: https://github.com/ietf-wg-httpapi/ratelimit-headers

    This is currently implemented by envoy-proxy, kong and 3scale.

    The spec

    defines 3 ratelimit-headers, returning the current quota, remaining quota and delta-seconds before quota expires.

       RateLimit-Limit: 100
       RateLimit-Remaining: 50
       RateLimit-Reset: 60
    
    
  • Ratelimit by header value creates keys using cached values

    Ratelimit by header value creates keys using cached values

    Hello! First of all, thanks for the great library. It worked great, until I had to add rate limiting by user id in the request header.

    I came across https://github.com/didip/tollbooth/issues/43, however I found that the function tollbooth.BuildKeys build keys using existing header values in the cache. Specifically this line: https://github.com/didip/tollbooth/blob/b10a036da5f05864224ee09432e489b32a6b2d1d/tollbooth.go#L89

    Should the key be created this way instead of looping through existing values?

    sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, r.Header.Get(headerKey)})
    

    As I understand it, this sliceKeys is then used to lookup against the rate limit cache. If sliceKeys contain existing headers, the current request would get rate limited due to one of the existing headers.

    If I have used the package wrongly, please help me to understand how I should use it.

    Here are the middleware I wrote and the test.

    func RateLimit(h http.HandlerFunc) http.HandlerFunc {
    	allocationLimiter := tollbooth.NewLimiter(1, &limiter.ExpirableOptions{DefaultExpirationTTL: time.Minute}).
    		SetMethods([]string{"POST"})
    
    	handler := func(w http.ResponseWriter, r *http.Request) {
    		customerID := r.Header.Get("X_Owner_ID")
    		fmt.Printf("customerID: %s\n", customerID)
    		allocationLimiter.SetHeader("X_Owner_ID", []string{customerID})
    
    		tollbooth.LimitFuncHandler(allocationLimiter, h).ServeHTTP(w, r)
    	}
    
    	return handler
    }
    
    func TestRateLimit(t *testing.T) {
    	customerID1 := "1234"
    	customerID2 := "5678"
    
    	tests := []struct {
    		name                string
    		secondRequestStatus int
    		customerIDs         []string
    	}{
    		{"different customer id", http.StatusOK, []string{customerID1, customerID2}},
    		{"same customer id", http.StatusTooManyRequests, []string{customerID1, customerID1}},
    	}
    	for _, tt := range tests {
    		t.Run(tt.name, func(t *testing.T) {
    			h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
    			testServer := httptest.NewServer(RateLimit(h))
    			defer testServer.Close()
    			client := &http.Client{}
    
    			request1, err := http.NewRequest("POST", testServer.URL, nil)
    			assert.Nil(t, err)
    			request1.Header.Add("CustomerID", tt.customerIDs[0])
    
    			response, err := client.Do(request1)
    			assert.Nil(t, err)
    			assert.Equal(t, http.StatusOK, response.StatusCode)
    
    			request2, err := http.NewRequest("POST", testServer.URL, nil)
    			assert.Nil(t, err)
    			request2.Header.Add("CustomerID", tt.customerIDs[1])
    
    			response, err = client.Do(request2)
    			assert.Nil(t, err)
    			assert.Equal(t, tt.secondRequestStatus, response.StatusCode)
    		})
    	}
    }
    
  • SetMethod is not working.

    SetMethod is not working.

    Hello there. I followed the guide and do the following:

    lmt.SetMethods([]string{"POST"})

    But it is not working. The limiter is still limiting the GET and all different kind of requests.

  • Is it possible to use tollbooth to limit memory usage?

    Is it possible to use tollbooth to limit memory usage?

    Is it possible to use tollbooth to limit total memory usage by my Go program? I would like to specify the maximum total memory that my go web application can use, such as, say 8GB, and if the memory has reach this limit, to not process the request. Thanks.

  • We received a vulnerability disclosure due to how we pick a remote IP address.

    We received a vulnerability disclosure due to how we pick a remote IP address.

    Disclosure URL: https://adam-p.ca/blog/2022/03/x-forwarded-for/

    This is an attempt to address the situation.

    1. We no longer configure SetIPLookups on default.

    2. We address the two different SetIPLookups confusion in two different place by removing both of them.

    3. We add a new, explicit way, for user to define how IP address should be picked up.

    Tests are all updated to use the new method of picking IP address.

    This will be a backward incompatible change so version number has to be bumped to 7.

  • Token limiting does not work.

    Token limiting does not work.

    Sample code:

    func limitAccess(next http.Handler) http.Handler {
    
    	lmt := tollbooth.NewLimiter(1, nil)
    	lmt.SetHeaderEntryExpirationTTL(time.Hour * 24)
    	lmt.SetHeader("X-Access-Token", []string{"abc123", "abc456"})
    	lmt.SetIPLookups([]string{"RemoteAddr", "X-Forwarded-For", "X-Real-IP"})
    	lmt.SetMethods([]string{"GET", "POST"})
    
    	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    		if httpError := tollbooth.LimitByRequest(lmt, w, r); httpError != nil {
    			http.Error(w, http.StatusText(401), http.StatusForbidden)
    			return
    		}
    
    		next.ServeHTTP(w, r)
    	})
    }
    
  • Share throttling information between multiple instances of an app

    Share throttling information between multiple instances of an app

    How can I share throttling information through multiple instances of an app that are managed by a load balancer?

    Issue https://github.com/didip/tollbooth/issues/57 answer mentions data is not persisted anywhere, therefore I can't plug a datastore (such as Redis) that would be used as a lookup store for all instances running at the same time.

    I have this scenario described above in my current architecture, where 3 or more instances of the app may be running and I would like to "share" throttling information between them in order to avoid having non-deterministic 429 requests depending on which box my request hits.

Simple, thread-safe Go rate-limiter

RateLimit Simple, thread-safe Go rate-limiter. Inspired by Antti Huima's algorithm on http://stackoverflow.com/a/668327 Example package main import (

Oct 16, 2022
Go http.Hander based middleware stack with context sharing

wrap Package wrap creates a fast and flexible middleware stack for http.Handlers. Features small; core is only 13 LOC based on http.Handler interface;

Apr 5, 2022
Minimalist net/http middleware for golang

interpose Interpose is a minimalist net/http middleware framework for golang. It uses http.Handler as its core unit of functionality, minimizing compl

Sep 27, 2022
Lightweight Middleware for net/http

MuxChain MuxChain is a small package designed to complement net/http for specifying chains of handlers. With it, you can succinctly compose layers of

Dec 10, 2022
Idiomatic HTTP Middleware for Golang

Negroni Notice: This is the library formerly known as github.com/codegangsta/negroni -- Github will automatically redirect requests to this repository

Jan 2, 2023
A tiny http middleware for Golang with added handlers for common needs.

rye A simple library to support http services. Currently, rye provides a middleware handler which can be used to chain http handlers together while pr

Jan 4, 2023
A collection of useful middleware for Go HTTP services & web applications 🛃

gorilla/handlers Package handlers is a collection of handlers (aka "HTTP middleware") for use with Go's net/http package (or any framework supporting

Dec 31, 2022
OpenID Connect (OIDC) http middleware for Go

Go OpenID Connect (OIDC) HTTP Middleware Introduction This is a middleware for http to make it easy to use OpenID Connect. Currently Supported framewo

Jan 1, 2023
Go HTTP middleware to filter clients by IP

Go HTTP middleware to filter clients by IP

Oct 30, 2022
Chi ip banner is a chi middleware that bans some ips from your Chi http server.

Chi Ip Banner Chi ip banner is a chi middleware that bans some ips from your Chi http server. It reads a .txt file in your project's root, called bani

Jan 4, 2022
Efficient token-bucket-based rate limiter package.

ratelimit -- import "github.com/juju/ratelimit" The ratelimit package provides an efficient token bucket implementation. See http://en.wikipedia.org/w

Dec 29, 2022
A timed rate limiter for Go

go-rate go-rate is a rate limiter designed for a range of use cases, including server side spam protection and preventing saturation of APIs you consu

Dec 17, 2022
Go package for rate limiter collection

rlc A rate limiter collection for Go. Pick up one of the rate limiters to throttle requests and control quota. RLC Slider TokenBucket RLC RLC is a rat

Jul 6, 2021
Painless middleware chaining for Go

Alice Alice provides a convenient way to chain your HTTP middleware functions and the app handler. In short, it transforms Middleware1(Middleware2(Mid

Dec 26, 2022
A Go middleware that stores various information about your web application (response time, status code count, etc.)

Go stats handler stats is a net/http handler in golang reporting various metrics about your web application. This middleware has been developed and re

Dec 10, 2022
gorilla/csrf provides Cross Site Request Forgery (CSRF) prevention middleware for Go web applications & services 🔒

gorilla/csrf gorilla/csrf is a HTTP middleware library that provides cross-site request forgery (CSRF) protection. It includes: The csrf.Protect middl

Jan 9, 2023
URL Rewrite middleware for gin

Url Rewrite middleware for gin Example In this exable these urls use the same route http://localhost:1234/test-me http://localhost:1234/index.php/test

Sep 15, 2022
A customized middleware of DAPR.

A customized middleware of DAPR.

Dec 24, 2021
Gin middleware for session.

wsession Gin middleware for session management with multi-backend support: cookie-based Redis memstore Usage Start using it Download and install it: g

Jan 9, 2022