https://github.com/logocomune/yarl
Golang rate limit
https://github.com/logocomune/yarl
golang rate-limit
Last synced: 2 months ago
JSON representation
Golang rate limit
- Host: GitHub
- URL: https://github.com/logocomune/yarl
- Owner: logocomune
- Created: 2020-04-27T12:37:52.000Z (about 6 years ago)
- Default Branch: master
- Last Pushed: 2026-02-20T18:42:37.000Z (4 months ago)
- Last Synced: 2026-02-20T23:52:33.648Z (4 months ago)
- Topics: golang, rate-limit
- Language: Go
- Homepage:
- Size: 67.4 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# YARL — Yet Another Rate Limiter
[](https://travis-ci.org/logocomune/yarl)
[](https://goreportcard.com/report/github.com/logocomune/yarl/v3)
[](https://codecov.io/gh/logocomune/yarl)
[](https://pkg.go.dev/github.com/logocomune/yarl/v3)
YARL is a Go library that implements **time-window rate limiting** with pluggable storage backends. It can limit any operation — HTTP requests, API calls, database writes — by counting how many times a given key has been used within a configurable time window.
---
## Features
- **Time-window based** — limits reset automatically at the end of each window (e.g. 100 req/minute)
- **Pluggable backends** — in-memory LRU cache or Redis via go-redis
- **Distributed-ready** — use Redis backends to share rate-limit counters across multiple instances
- **Flexible key** — limit by IP address, request headers, user ID, or any combination
- **Standard HTTP headers** — middleware sets `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After`
- **Framework integrations** — drop-in middleware for the standard `net/http` package and the [Gin](https://github.com/gin-gonic/gin) framework
---
## Installation
```bash
go get github.com/logocomune/yarl/v3
```
---
## Quick Start
### Core library
```go
package main
import (
"fmt"
"time"
"github.com/logocomune/yarl/v3"
"github.com/logocomune/yarl/v3/integration/limiter/lruyarl"
)
func main() {
// Create an in-memory LRU backend (1 000 tracked keys max)
backend, err := lruyarl.New(1000)
if err != nil {
panic(err)
}
// Allow 5 operations per 10 seconds, namespaced under "myapp"
limiter := yarl.New("myapp", backend, 5, 10*time.Second)
for i := 1; i <= 7; i++ {
resp, err := limiter.IsAllow("user:42")
if err != nil {
panic(err)
}
if resp.IsAllowed {
fmt.Printf("Request %d: ALLOWED (remaining: %d)\n", i, resp.Remain)
} else {
fmt.Printf("Request %d: DENIED (retry after %ds)\n", i, resp.RetryAfter)
}
}
}
```
**Output** (first 5 requests are allowed, the next 2 are denied):
```
Request 1: ALLOWED (remaining: 4)
Request 2: ALLOWED (remaining: 3)
Request 3: ALLOWED (remaining: 2)
Request 4: ALLOWED (remaining: 1)
Request 5: ALLOWED (remaining: 0)
Request 6: DENIED (retry after 8s)
Request 7: DENIED (retry after 7s)
```
---
## Backends
### In-Memory LRU (single process)
```go
import "github.com/logocomune/yarl/v3/integration/limiter/lruyarl"
backend, err := lruyarl.New(1000) // track up to 1 000 keys
```
The LRU cache is bounded: when full it evicts the least recently used key. Suitable for single-process deployments where counters can be lost on restart.
---
### Redis — go-redis (recommended)
```go
import (
"github.com/redis/go-redis/v9"
"github.com/logocomune/yarl/v3/integration/limiter/goredisyarl"
)
client := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
backend := goredisyarl.NewPool(client)
```
Middleware helpers also support both ownership models: use
`NewConfigurationWithGoRedis(...)` to let YARL create the Redis client, or
`NewConfigurationWithRedisClient(...)` to reuse an existing client. If YARL
creates the client internally, call `conf.Close()` during shutdown.
---
## HTTP Middleware (`net/http`)
The `httpratelimit` package wraps any `http.HandlerFunc`:
```go
package main
import (
"net/http"
"time"
"github.com/logocomune/yarl/v3/integration/httpratelimit"
)
func main() {
// 100 requests per minute, in-memory LRU, limit by client IP
conf := httpratelimit.NewConfigurationWithLru("api", 10000, 100, time.Minute)
conf.UseIP = true
handler := httpratelimit.New(conf, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello!"))
})
http.ListenAndServe(":8080", handler)
}
```
### Rate limit by IP + custom header
```go
conf := httpratelimit.NewConfigurationWithLru("api", 10000, 100, time.Minute)
conf.UseIP = true
conf.Headers = []string{"X-Tenant-ID"} // separate bucket per tenant
```
### Using a Redis backend (go-redis)
```go
conf := httpratelimit.NewConfigurationWithGoRedis(
"api", // prefix
"localhost:6379", // Redis address
0, // Redis DB
100, // max requests
time.Minute, // window
)
defer conf.Close()
conf.UseIP = true
```
### Response headers set by the middleware
| Header | Description |
|---|---|
| `X-RateLimit-Limit` | Maximum requests allowed in the window |
| `X-RateLimit-Remaining` | Requests remaining in the current window |
| `X-RateLimit-Reset` | Unix timestamp when the current window resets |
| `Retry-After` | Seconds until the next window (only on HTTP 429) |
---
## Gin Middleware
```go
package main
import (
"time"
"github.com/gin-gonic/gin"
"github.com/logocomune/yarl/v3/integration/ginratelimit"
"github.com/logocomune/yarl/v3/integration/limiter/lruyarl"
)
func main() {
backend, _ := lruyarl.New(10000)
conf := ginratelimit.NewConfiguration("api", backend, 100, time.Minute)
conf.UseIP = true
conf.Headers = []string{"X-Tenant-ID"}
r := gin.Default()
r.Use(ginratelimit.New(conf))
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
r.Run(":8080")
}
```
Or use the built-in Redis helper:
```go
conf := ginratelimit.NewConfigurationWithGoRedis(
"api", // prefix
"localhost:6379", // Redis address
0, // Redis DB
100, // max requests
time.Minute, // window
)
defer conf.Close()
conf.UseIP = true
r.Use(ginratelimit.New(conf))
```
---
## API Reference
### `yarl.New`
```go
func New(prefix string, l Limiter, max int64, timeWindow time.Duration) Yarl
```
Creates a new rate limiter.
| Parameter | Description |
|---|---|
| `prefix` | Namespace for cache keys (avoids collisions when sharing a backend) |
| `l` | Storage backend implementing the `Limiter` interface |
| `max` | Maximum number of operations allowed per `timeWindow` |
| `timeWindow` | Duration of each rate-limit window |
### `Yarl.IsAllow`
```go
func (y *Yarl) IsAllow(key string) (*Resp, error)
```
Checks whether the operation identified by `key` is within the configured limit. Returns `*Resp` on success.
### `Yarl.IsAllowWithLimit`
```go
func (y *Yarl) IsAllowWithLimit(key string, max int64, tWindow time.Duration) (*Resp, error)
```
Same as `IsAllow` but overrides `max` and `tWindow` for this specific call. Useful when different callers need different limits on a shared `Yarl` instance.
### `Resp` fields
| Field | Type | Description |
|---|---|---|
| `IsAllowed` | `bool` | `true` if the request is within the limit |
| `Current` | `int64` | Counter value after this request |
| `Max` | `int64` | Configured limit |
| `Remain` | `int64` | Requests remaining in the current window |
| `NextReset` | `int64` | Unix timestamp of the next window reset |
| `RetryAfter` | `int64` | Seconds until the next reset |
### `Limiter` interface
```go
type Limiter interface {
Inc(key string, ttlSeconds int64) (int64, error)
}
```
Implement this interface to plug in a custom backend (e.g. Memcached, DynamoDB).
---
## Custom Backend Example
```go
type MyBackend struct{ /* ... */ }
func (b *MyBackend) Inc(key string, ttlSeconds int64) (int64, error) {
// atomically increment counter for key, set TTL, return new value
return newValue, nil
}
limiter := yarl.New("prefix", &MyBackend{}, 100, time.Minute)
```
---
## Test Coverage
| Package | Coverage |
|---|---|
| `yarl` (core) | **100%** |
| `lruyarl` | **100%** |
| `radixyarl` | 83% |
| `goredisyarl` | 82% |
| `redigoyarl` | 80% |
| `ginratelimit` | 79% |
| `httpratelimit` | 77% |
> Redis-backend packages have lower coverage because the `NewConfigurationWithRadix` factory functions require a live Redis server and are not exercised in unit tests.
---
## License
MIT