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.

An HTTP client for go-server-timing middleware. Enables automatic timing propagation through HTTP calls between servers.

client-timing An HTTP client for go-server-timing middleware. Features: An HTTP Client or RoundTripper, fully compatible with Go's standard library. A

Dec 1, 2022
Fault injection library in Go using standard http middleware

Fault The fault package provides go http middleware that makes it easy to inject faults into your service. Use the fault package to reject incoming re

Nov 20, 2022
Go net/http configurable handler to handle CORS requests

Go CORS handler CORS is a net/http handler implementing Cross Origin Resource Sharing W3 specification in Golang. Getting Started After installing Go

Nov 25, 2022
Go middleware for monetizing your API on a per-request basis with Bitcoin and Lightning ⚡️

ln-paywall Go middleware for monetizing your API on a per-request basis with Bitcoin and Lightning ⚡️ Middlewares for: net/http HandlerFunc net/http H

Nov 25, 2022
A Golang Middleware to handle X-Forwarded-For Header

X-Forwarded-For middleware fo Go Package xff is a net/http middleware/handler to parse Forwarded HTTP Extension in Golang. Example usage Install xff:

Nov 3, 2022
Go net/http handler to transparently manage posted JSON

Go JSON handler FormJSON is a net/http handler implementing content negotiation for posted data in order to transparently expose posted JSON as if it

Sep 27, 2022
Go (golang) library for creating and consuming HTTP Server-Timing headers
Go (golang) library for creating and consuming HTTP Server-Timing headers

HTTP Server-Timing for Go This is a library including middleware for using HTTP Server-Timing with Go. This header allows a server to send timing info

Nov 19, 2022
哪吒监控 一站式轻监控轻运维系统。支持系统状态、HTTP、TCP、Ping 监控报警,命令批量执行和计划任务
哪吒监控 一站式轻监控轻运维系统。支持系统状态、HTTP、TCP、Ping 监控报警,命令批量执行和计划任务

哪吒监控 一站式轻监控轻运维系统。支持系统状态、HTTP、TCP、Ping 监控报警,命令批量执行和计划任务

Nov 28, 2022
Restish is a CLI for interacting with REST-ish HTTP APIs with some nice features built-in
Restish is a CLI for interacting with REST-ish HTTP APIs with some nice features built-in

Restish is a CLI for interacting with REST-ish HTTP APIs with some nice features built-in, like always having the latest API resources, fields, and operations available when they go live on the API without needing to install or update anything.

Dec 2, 2022
A dead simple parser package for Go
A dead simple parser package for Go

A dead simple parser package for Go

Nov 26, 2022
Redcon is a custom Redis server framework for Go that is fast and simple to use.
Redcon is a custom Redis server framework for Go that is fast and simple to use.

Redcon is a custom Redis server framework for Go that is fast and simple to use. The reason for this library it to give an efficient server front-end for the BuntDB and Tile38 projects.

Dec 1, 2022
A simple and lightweight encrypted password manager written in Go.
A simple and lightweight encrypted password manager written in Go.

A simple and lightweight encrypted password manager written in Go.

Jun 16, 2022
Remark42 is a self-hosted, lightweight, and simple comment engine
Remark42 is a self-hosted, lightweight, and simple comment engine

Remark42 is a self-hosted, lightweight, and simple (yet functional) comment engine, which doesn't spy on users. It can be embedded into blogs, articles or any other place where readers add comments.

Nov 28, 2022
Simple middleware to rate-limit HTTP requests.

Tollbooth This is a generic middleware to rate-limit HTTP requests. NOTE 1: This library is considered finished. NOTE 2: Major version changes are bac

Dec 2, 2022
Docker-hub-rate-limit - Show pulling rate status of Docker-hub

Docker-Hub Pull Rate Status This tool shows current status of docker hub pull ra

Jan 28, 2022
Pacemaker - Rate limit library. Currently implemented rate limits are

PaceMaker Rate limit library. Currently implemented rate limits are Fixed window

Nov 5, 2022
Dead simple rate limit middleware for Go.

Limiter Dead simple rate limit middleware for Go. Simple API "Store" approach for backend Redis support (but not tied too) Middlewares: HTTP, FastHTTP

Nov 23, 2022
Dead simple rate limit middleware for Go.

Limiter Dead simple rate limit middleware for Go. Simple API "Store" approach for backend Redis support (but not tied too) Middlewares: HTTP, FastHTTP

Nov 29, 2022
Limit-order-book - Limit order books keep records of orders for a given symbol to be traded

Limit Order Book Limit order books keep records of orders for a given symbol to

Jan 17, 2022
A Golang blocking leaky-bucket rate limit implementation

Go rate limiter This package provides a Golang implementation of the leaky-bucket rate limit algorithm. This implementation refills the bucket based o

Nov 28, 2022