{"id":19715922,"url":"https://github.com/logocomune/yarl","last_synced_at":"2026-04-13T19:01:06.135Z","repository":{"id":95246558,"uuid":"259312902","full_name":"logocomune/yarl","owner":"logocomune","description":"Golang rate limit","archived":false,"fork":false,"pushed_at":"2026-02-20T18:42:37.000Z","size":69,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-02-20T23:52:33.648Z","etag":null,"topics":["golang","rate-limit"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/logocomune.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-04-27T12:37:52.000Z","updated_at":"2026-02-20T18:42:08.000Z","dependencies_parsed_at":"2024-06-20T04:43:22.458Z","dependency_job_id":null,"html_url":"https://github.com/logocomune/yarl","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/logocomune/yarl","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/logocomune%2Fyarl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/logocomune%2Fyarl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/logocomune%2Fyarl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/logocomune%2Fyarl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/logocomune","download_url":"https://codeload.github.com/logocomune/yarl/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/logocomune%2Fyarl/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31766482,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-13T15:25:13.801Z","status":"ssl_error","status_checked_at":"2026-04-13T15:25:09.162Z","response_time":93,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["golang","rate-limit"],"created_at":"2024-11-11T22:39:50.814Z","updated_at":"2026-04-13T19:01:06.130Z","avatar_url":"https://github.com/logocomune.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# YARL — Yet Another Rate Limiter\n\n[![Build Status](https://travis-ci.org/logocomune/yarl.svg?branch=master)](https://travis-ci.org/logocomune/yarl)\n[![Go Report Card](https://goreportcard.com/badge/github.com/logocomune/yarl/v3)](https://goreportcard.com/report/github.com/logocomune/yarl/v3)\n[![codecov](https://codecov.io/gh/logocomune/yarl/branch/master/graph/badge.svg)](https://codecov.io/gh/logocomune/yarl)\n[![Go Reference](https://pkg.go.dev/badge/github.com/logocomune/yarl/v3.svg)](https://pkg.go.dev/github.com/logocomune/yarl/v3)\n\nYARL 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.\n\n---\n\n## Features\n\n- **Time-window based** — limits reset automatically at the end of each window (e.g. 100 req/minute)\n- **Pluggable backends** — in-memory LRU cache or Redis via go-redis\n- **Distributed-ready** — use Redis backends to share rate-limit counters across multiple instances\n- **Flexible key** — limit by IP address, request headers, user ID, or any combination\n- **Standard HTTP headers** — middleware sets `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After`\n- **Framework integrations** — drop-in middleware for the standard `net/http` package and the [Gin](https://github.com/gin-gonic/gin) framework\n\n---\n\n## Installation\n\n```bash\ngo get github.com/logocomune/yarl/v3\n```\n\n---\n\n## Quick Start\n\n### Core library\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"time\"\n\n    \"github.com/logocomune/yarl/v3\"\n    \"github.com/logocomune/yarl/v3/integration/limiter/lruyarl\"\n)\n\nfunc main() {\n    // Create an in-memory LRU backend (1 000 tracked keys max)\n    backend, err := lruyarl.New(1000)\n    if err != nil {\n        panic(err)\n    }\n\n    // Allow 5 operations per 10 seconds, namespaced under \"myapp\"\n    limiter := yarl.New(\"myapp\", backend, 5, 10*time.Second)\n\n    for i := 1; i \u003c= 7; i++ {\n        resp, err := limiter.IsAllow(\"user:42\")\n        if err != nil {\n            panic(err)\n        }\n        if resp.IsAllowed {\n            fmt.Printf(\"Request %d: ALLOWED  (remaining: %d)\\n\", i, resp.Remain)\n        } else {\n            fmt.Printf(\"Request %d: DENIED   (retry after %ds)\\n\", i, resp.RetryAfter)\n        }\n    }\n}\n```\n\n**Output** (first 5 requests are allowed, the next 2 are denied):\n\n```\nRequest 1: ALLOWED  (remaining: 4)\nRequest 2: ALLOWED  (remaining: 3)\nRequest 3: ALLOWED  (remaining: 2)\nRequest 4: ALLOWED  (remaining: 1)\nRequest 5: ALLOWED  (remaining: 0)\nRequest 6: DENIED   (retry after 8s)\nRequest 7: DENIED   (retry after 7s)\n```\n\n---\n\n## Backends\n\n### In-Memory LRU (single process)\n\n```go\nimport \"github.com/logocomune/yarl/v3/integration/limiter/lruyarl\"\n\nbackend, err := lruyarl.New(1000) // track up to 1 000 keys\n```\n\nThe 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.\n\n---\n\n### Redis — go-redis (recommended)\n\n```go\nimport (\n    \"github.com/redis/go-redis/v9\"\n    \"github.com/logocomune/yarl/v3/integration/limiter/goredisyarl\"\n)\n\nclient := redis.NewClient(\u0026redis.Options{Addr: \"localhost:6379\"})\nbackend := goredisyarl.NewPool(client)\n```\n\nMiddleware helpers also support both ownership models: use\n`NewConfigurationWithGoRedis(...)` to let YARL create the Redis client, or\n`NewConfigurationWithRedisClient(...)` to reuse an existing client. If YARL\ncreates the client internally, call `conf.Close()` during shutdown.\n\n---\n\n## HTTP Middleware (`net/http`)\n\nThe `httpratelimit` package wraps any `http.HandlerFunc`:\n\n```go\npackage main\n\nimport (\n    \"net/http\"\n    \"time\"\n\n    \"github.com/logocomune/yarl/v3/integration/httpratelimit\"\n)\n\nfunc main() {\n    // 100 requests per minute, in-memory LRU, limit by client IP\n    conf := httpratelimit.NewConfigurationWithLru(\"api\", 10000, 100, time.Minute)\n    conf.UseIP = true\n\n    handler := httpratelimit.New(conf, func(w http.ResponseWriter, r *http.Request) {\n        w.Write([]byte(\"Hello!\"))\n    })\n\n    http.ListenAndServe(\":8080\", handler)\n}\n```\n\n### Rate limit by IP + custom header\n\n```go\nconf := httpratelimit.NewConfigurationWithLru(\"api\", 10000, 100, time.Minute)\nconf.UseIP = true\nconf.Headers = []string{\"X-Tenant-ID\"} // separate bucket per tenant\n```\n\n### Using a Redis backend (go-redis)\n\n```go\nconf := httpratelimit.NewConfigurationWithGoRedis(\n    \"api\",            // prefix\n    \"localhost:6379\", // Redis address\n    0,                // Redis DB\n    100,              // max requests\n    time.Minute,      // window\n)\ndefer conf.Close()\nconf.UseIP = true\n```\n\n### Response headers set by the middleware\n\n| Header | Description |\n|---|---|\n| `X-RateLimit-Limit` | Maximum requests allowed in the window |\n| `X-RateLimit-Remaining` | Requests remaining in the current window |\n| `X-RateLimit-Reset` | Unix timestamp when the current window resets |\n| `Retry-After` | Seconds until the next window (only on HTTP 429) |\n\n---\n\n## Gin Middleware\n\n```go\npackage main\n\nimport (\n    \"time\"\n\n    \"github.com/gin-gonic/gin\"\n    \"github.com/logocomune/yarl/v3/integration/ginratelimit\"\n    \"github.com/logocomune/yarl/v3/integration/limiter/lruyarl\"\n)\n\nfunc main() {\n    backend, _ := lruyarl.New(10000)\n    conf := ginratelimit.NewConfiguration(\"api\", backend, 100, time.Minute)\n    conf.UseIP = true\n    conf.Headers = []string{\"X-Tenant-ID\"}\n\n    r := gin.Default()\n    r.Use(ginratelimit.New(conf))\n\n    r.GET(\"/ping\", func(c *gin.Context) {\n        c.JSON(200, gin.H{\"message\": \"pong\"})\n    })\n\n    r.Run(\":8080\")\n}\n```\n\nOr use the built-in Redis helper:\n\n```go\nconf := ginratelimit.NewConfigurationWithGoRedis(\n    \"api\",            // prefix\n    \"localhost:6379\", // Redis address\n    0,                // Redis DB\n    100,              // max requests\n    time.Minute,      // window\n)\ndefer conf.Close()\nconf.UseIP = true\nr.Use(ginratelimit.New(conf))\n```\n\n---\n\n## API Reference\n\n### `yarl.New`\n\n```go\nfunc New(prefix string, l Limiter, max int64, timeWindow time.Duration) Yarl\n```\n\nCreates a new rate limiter.\n\n| Parameter | Description |\n|---|---|\n| `prefix` | Namespace for cache keys (avoids collisions when sharing a backend) |\n| `l` | Storage backend implementing the `Limiter` interface |\n| `max` | Maximum number of operations allowed per `timeWindow` |\n| `timeWindow` | Duration of each rate-limit window |\n\n### `Yarl.IsAllow`\n\n```go\nfunc (y *Yarl) IsAllow(key string) (*Resp, error)\n```\n\nChecks whether the operation identified by `key` is within the configured limit. Returns `*Resp` on success.\n\n### `Yarl.IsAllowWithLimit`\n\n```go\nfunc (y *Yarl) IsAllowWithLimit(key string, max int64, tWindow time.Duration) (*Resp, error)\n```\n\nSame as `IsAllow` but overrides `max` and `tWindow` for this specific call. Useful when different callers need different limits on a shared `Yarl` instance.\n\n### `Resp` fields\n\n| Field | Type | Description |\n|---|---|---|\n| `IsAllowed` | `bool` | `true` if the request is within the limit |\n| `Current` | `int64` | Counter value after this request |\n| `Max` | `int64` | Configured limit |\n| `Remain` | `int64` | Requests remaining in the current window |\n| `NextReset` | `int64` | Unix timestamp of the next window reset |\n| `RetryAfter` | `int64` | Seconds until the next reset |\n\n### `Limiter` interface\n\n```go\ntype Limiter interface {\n    Inc(key string, ttlSeconds int64) (int64, error)\n}\n```\n\nImplement this interface to plug in a custom backend (e.g. Memcached, DynamoDB).\n\n---\n\n## Custom Backend Example\n\n```go\ntype MyBackend struct{ /* ... */ }\n\nfunc (b *MyBackend) Inc(key string, ttlSeconds int64) (int64, error) {\n    // atomically increment counter for key, set TTL, return new value\n    return newValue, nil\n}\n\nlimiter := yarl.New(\"prefix\", \u0026MyBackend{}, 100, time.Minute)\n```\n\n---\n\n## Test Coverage\n\n| Package | Coverage |\n|---|---|\n| `yarl` (core) | **100%** |\n| `lruyarl` | **100%** |\n| `radixyarl` | 83% |\n| `goredisyarl` | 82% |\n| `redigoyarl` | 80% |\n| `ginratelimit` | 79% |\n| `httpratelimit` | 77% |\n\n\u003e Redis-backend packages have lower coverage because the `NewConfigurationWithRadix` factory functions require a live Redis server and are not exercised in unit tests.\n\n---\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flogocomune%2Fyarl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flogocomune%2Fyarl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flogocomune%2Fyarl/lists"}