commit
eba33751ed
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 William Perron
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
@ -0,0 +1,3 @@
|
|||||||
|
# HTTP Rate Limiter
|
||||||
|
|
||||||
|
An HTTP Handler that implements rate limiting.
|
@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.wperron.io/ratelimit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
hello := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprintf(w, "Hello, World!")
|
||||||
|
})
|
||||||
|
|
||||||
|
// A simple Hello World handler wrapped in a RateLimitedHandler
|
||||||
|
l := ratelimit.NewLimiter(1_000, time.Second)
|
||||||
|
handler := &ratelimit.RateLimitedHandler{
|
||||||
|
Next: hello,
|
||||||
|
Limiter: l,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Starting server on :8080")
|
||||||
|
if err := http.ListenAndServe(":8080", handler); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
log.Println("Server stopped")
|
||||||
|
}
|
@ -0,0 +1,104 @@
|
|||||||
|
package ratelimit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Limiter struct {
|
||||||
|
current atomic.Uint64
|
||||||
|
last atomic.Uint64
|
||||||
|
limit int // number of requests per evaluation window
|
||||||
|
reset atomic.Int64 // time of last reset in UnixMicro
|
||||||
|
windowMicros int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLimiter(limit int, window time.Duration) *Limiter {
|
||||||
|
l := &Limiter{
|
||||||
|
limit: limit,
|
||||||
|
reset: atomic.Int64{},
|
||||||
|
windowMicros: window.Microseconds(),
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// If the current time in microseconds exceeds the last reset time by more
|
||||||
|
// than the evaluation window, attempt to reset the limiter. This is done by
|
||||||
|
// atomically comparing and swapping the reset time. If successful, store
|
||||||
|
// the current count in the last count and reset the current count to 0.
|
||||||
|
t := time.NewTicker(window)
|
||||||
|
for now := range t.C {
|
||||||
|
l.reset.Store(now.UnixMicro())
|
||||||
|
l.last.Store(l.current.Swap(0))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow returns true if the current rate of requests is below the limit set by the
|
||||||
|
// Limiter.
|
||||||
|
//
|
||||||
|
// rate calculates the current rate of some process based on the last recorded rate,
|
||||||
|
// the time elapsed since the last reset, and the current value. It uses a weighted
|
||||||
|
// average approach where the weight is determined by the proportion of the time
|
||||||
|
// remaining in the current minute.
|
||||||
|
//
|
||||||
|
// The formula is:
|
||||||
|
// rate = (lastValue * (remainingTime / evaluationWindow)) + currentValue
|
||||||
|
//
|
||||||
|
// For example, imagine we set a limit of 50 requests per minute and the evaluation
|
||||||
|
// window is 60 seconds. We recorded 42 requests during the last evaluation window,
|
||||||
|
// and 18 requests have been recorded after 15 seconds in the current evaluation window.
|
||||||
|
// The rate would be calculated as follows:
|
||||||
|
//
|
||||||
|
// rate = 42 * ((60-15)/60) + 18
|
||||||
|
//
|
||||||
|
// = 42 * 0.75 + 18
|
||||||
|
// = 49.5 requests
|
||||||
|
//
|
||||||
|
// - lastValue: The count recorded during the last evaluation window.
|
||||||
|
// - remainingTime: The time remaining in the current evaluation window (in microseconds).
|
||||||
|
// - evalutationWindow: The duration of the evaluation window (in microseconds).
|
||||||
|
// - currentValue: The value of the current evaluation window.
|
||||||
|
//
|
||||||
|
// This approach ensures that the rate smoothly transitions from the last recorded
|
||||||
|
// rate to the current value as time progresses within the evaluation window.
|
||||||
|
//
|
||||||
|
// Credit to CloudFlare for the idea.
|
||||||
|
// see: https://blog.cloudflare.com/counting-things-a-lot-of-different-things/
|
||||||
|
func (l *Limiter) Allow() bool {
|
||||||
|
last := float64(l.last.Load())
|
||||||
|
current := float64(l.current.Load())
|
||||||
|
nowMicros := time.Now().UnixMicro()
|
||||||
|
resetMicros := l.reset.Load()
|
||||||
|
elapsed := nowMicros - resetMicros
|
||||||
|
|
||||||
|
rate := (last * (float64(l.windowMicros-(elapsed)) / float64(l.windowMicros))) + current
|
||||||
|
|
||||||
|
return rate <= float64(l.limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RateLimitedHandler struct {
|
||||||
|
Next http.Handler
|
||||||
|
Limiter *Limiter
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP implements the http.Handler interface for RateLimitedHandler. It
|
||||||
|
// checks the current rate of requests against the limit set by the Limiter. If
|
||||||
|
// the rate exceeds the limit, it returns a 429 Too Many Requests status code.
|
||||||
|
// Otherwise, it calls the Next handler in the chain.
|
||||||
|
func (rlh *RateLimitedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
l := rlh.Limiter
|
||||||
|
|
||||||
|
if !l.Allow() {
|
||||||
|
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asynchronously increment the current count to avoid blocking the request.
|
||||||
|
// This is done after the request has been allowed to proceed to ensure that
|
||||||
|
// the rate is calculated correctly.
|
||||||
|
rlh.Next.ServeHTTP(w, r)
|
||||||
|
go l.current.Add(1)
|
||||||
|
}
|
Loading…
Reference in new issue