Simple middleware to rate-limit HTTP requests.

Related tags

tollbooth
Overview

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.

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

    opened by zwopir 27
  • 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.

    opened by tylerstillwater 17
  • 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.

    opened by benguild 13
  • 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.

    opened by paskal 12
  • 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.

    opened by yanfali 11
  • 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!

    opened by tylerstillwater 9
  • 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)
    		})
    	}
    }
    
    opened by caalberts 8
  • Cleanup tokenBuckets

    Cleanup tokenBuckets

    Hi, I was just browsing the code and have one question:

    Shouldn't tokenBuckets map in Limiter be cleaned up from old limiters from time to time?

    opened by janekolszak 6
  • 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.

    opened by jasonkwh 6
  • 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.

    opened by msaron 6
  • 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://tools.ietf.org/html/draft-polli-ratelimit-headers-03

    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
    
    
    opened by ioggstream 4
  • Thank you!

    Thank you!

    Thank you very much for this package

    opened by lfaoro 2
  • 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.

    opened by matbhz 1
  • Refactor to make it usable for grpc-gateway and other use cases (not simple http server)

    Refactor to make it usable for grpc-gateway and other use cases (not simple http server)

    The output of LimitByRequest is changed. Also it doesn't set http headers (they are set in LimitHander) Those are the only compatibility breaks.

    opened by ilius 0
Owner
Didip Kerabat
Didip Kerabat
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

null 7k Jul 23, 2021
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

InVision 97 Jun 6, 2021
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

Michael Alexander 338 Jul 15, 2021
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

Didip Kerabat 2k Jul 13, 2021
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

Juju 2k Jul 26, 2021
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

Florent Messa 579 Jul 1, 2021
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

Gorilla Web Toolkit 676 Jul 19, 2021
A golang framework like koa.js

koa.go Expressive HTTP middleware framework for Golang to make web applications and APIs more enjoyable to write like Koa.js. Koa's middleware stack f

Ya Hui Liang(Ryou) 17 Jul 6, 2021
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;

go-on - web toolkit in Go 59 Aug 28, 2020
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

Justinas Stankevičius 2.3k Jul 13, 2021
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

Uber Go 2.4k Jul 22, 2021
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

Alex Xu 15 Jul 6, 2021
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 (

Black Square Media 66 May 3, 2021
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

Gorilla Web Toolkit 1.2k Jul 25, 2021