{"id":19873962,"url":"https://github.com/bose/golimiter","last_synced_at":"2026-05-10T12:02:08.283Z","repository":{"id":97217561,"uuid":"204778944","full_name":"Bose/golimiter","owner":"Bose","description":"rate limiter library that supports in-memory and distributed eventually consistent redis stores (includes Gin middleware)","archived":false,"fork":false,"pushed_at":"2020-10-07T18:36:00.000Z","size":605,"stargazers_count":0,"open_issues_count":1,"forks_count":1,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-01-11T16:50:12.875Z","etag":null,"topics":["go","in-memory","rate-limiter","redis"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Bose.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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":"2019-08-27T19:49:18.000Z","updated_at":"2019-11-06T13:42:00.000Z","dependencies_parsed_at":null,"dependency_job_id":"f1b46bd2-a9bf-4455-8a66-a9cd81166fc8","html_url":"https://github.com/Bose/golimiter","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bose%2Fgolimiter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bose%2Fgolimiter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bose%2Fgolimiter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bose%2Fgolimiter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Bose","download_url":"https://codeload.github.com/Bose/golimiter/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241301726,"owners_count":19940696,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["go","in-memory","rate-limiter","redis"],"created_at":"2024-11-12T16:20:42.820Z","updated_at":"2026-05-10T12:02:03.238Z","avatar_url":"https://github.com/Bose.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# golimiter\n[![](https://godoc.org/github.com/Bose/golimiter?status.svg)](https://github.com/Bose/golimiter/blob/master/godoc.md) \n\u003cimg align=\"right\" width=\"200px\" src=\"./images/go_build.png\" /\u003e\n\nA rate limiter for Go that uses a fixed-window approach to tracking rates.  Each incoming request increments the counter of the window.  If the counter exceeds the limit, the request is denied.   \n\nCounter storage: golimiter's implementation uses an in memory store (LRU with expiry and a bound upper limit of entries). It also also includes a distributed counter store using Redis.  For performance reasons, this distributed store will be eventually consistent with the in memory store.  The centralized store will be updated using a set-then-get approach, relying on atomic Redis operators to eliminate race conditions under high-concurrency.  The central store is optional... if it's not configured, then golimiter is happy to just keep working by only using it's in memory store.\n\n## Middleware\nGolimiter includes Gin middleware and an example how-to service. \n\n## Installation\n\n`$ go get github.com/Bose/golimiter`\n\nYou'll also want to install go-cache for counter storage.\n\n`$ go get \"github.com/Bose/go-cache`\n\n## Benchmarks (for the rate limiter itself)\n```bash\n$ ./run-benchmarks.sh \ngoos: darwin\ngoarch: amd64\npkg: github.com/Bose/golimiter\nBenchmarkIncrInMemory1-8    \t 1000000\t      1903 ns/op\nBenchmarkIncrInMemory2-8    \t 1000000\t      1910 ns/op\nBenchmarkIncrInMemory3-8    \t 1000000\t      1929 ns/op\nBenchmarkIncrInMemory10-8   \t 1000000\t      1912 ns/op\nBenchmarkIncrInMemory20-8   \t 1000000\t      1912 ns/op\nBenchmarkIncrInMemory40-8   \t 1000000\t      1909 ns/op\nPASS\nok  \tgithub.com/Bose/golimiter\t12.620s\n```\n\n## Usage (general)\n```go\npackage main\n\n// Create a rate with the given limit (number of requests) for the given\n// period (a time.Duration of your choice).\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\tgoCache \"github.com/Bose/go-cache\"\n\t\"github.com/Bose/golimiter\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nconst (\n\tmaxEntriesInMemory    = 100\n\tredisTestServer       = \"redis://localhost:6379\"\n\tsentinelTestServer    = \"redis://localhost:26379\"\n\tredisMasterIdentifier = \"mymaster\"\n\tsharedSecret          = \"test-secret-must\"\n\tuseSentinel           = true\n\tdefExpSeconds         = 0\n\t//myEnv                 = \"local\"\n\tmaxConnectionsAllowed = 5\n\tredisOpsTimeout       = 50  // millisecods\n\tredisConnTimeout      = 500 // milliseconds\n)\n\nfunc main() {\n\tlogrus.SetLevel(logrus.DebugLevel)\n\tlogger := logrus.WithFields(logrus.Fields{\"method\": \"main\"})\n\n\trate := golimiter.Rate{\n\t\tPeriod: 1 * time.Hour,\n\t\tLimit:  1000,\n\t}\n\n\tlogger.Debug(\"setting envVar TRACE_SYNC= true will turn-on trace logging to the central store for the limiters, you must call this before creating any limiters\")\n\tos.Setenv(\"TRACE_SYNC\", \"true\")\n\n\tctx := context.Background()\n\n\trateStore := golimiter.New(golimiter.NewInMemoryLimiterStore(maxEntriesInMemory, golimiter.WithLimiterExpiration(rate.Period)), rate)\n\n\tc, err := rateStore.Get(ctx, \"test-rate-object\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Printf(\"Limit: %d\\nReached: %v\\nRemaining: %d\\nReset: %d\\n\", c.Limit, c.Reached, c.Remaining, c.Reset)\n\n\t// You can also use the simplified format \"\u003climit\u003e-\u003cduration\u003e-\u003cperiod\u003e\"\", with the given\n\t// periods:\n\t//\n\t// * \"S\": second\n\t// * \"M\": minute\n\t// * \"H\": hour\n\t//\n\t// You can also add an optional delay to the format \"\u003climit\u003e-\u003cduration\u003e-\u003cperiod\u003e-\u003cdelay\u003e\":\n\t// \"1-1-S-10\"  represents 1 req in 1 second with 10ms delay\n\t//\n\t// Examples:\n\t//\n\t// * 5 reqs in 10 seconds: \"5-10-S\"\n\t// * 10 reqs in 5 minutes: \"10-5-M\"\n\t// * 1000 reqs in 1 hour: \"1000-1-H\"\n\t// * 5 reqs in 10 seconds with 20ms delay: \"5-10-S-20\"\n\t//\n\n\trate, err = golimiter.NewRateFromFormatted(\"1000-1-H\")\n\tif err != nil {\n\t\tpanic(err)\n\n\t}\n\n\t// let's make a limiter that uses redis for cross process coordiation\n\tstore := golimiter.NewInMemoryLimiterStore(maxEntriesInMemory, golimiter.WithLimiterExpiration(rate.Period))\n\tstore.DistributedRedisCache = setupRedisCentralStore(logger)\n\trateStore = golimiter.New(store, rate)\n\tc, err = rateStore.Get(ctx, \"test-rate-object\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Printf(\"Limit: %d\\nReached: %v\\nRemaining: %d\\nReset: %d\\nDelay: %d\\n\", c.Limit, c.Reached, c.Remaining, c.Reset, c.Delay)\n\ttime.Sleep(500 * time.Millisecond) // since it's eventually consistent with redis, you need to give it a sec to sync\n\n\trateWithDelay, err := golimiter.NewRateFromFormatted(\"1000-1-H-1000\")\n\tif err != nil {\n\t\tpanic(err)\n\n\t}\n\trateStore = golimiter.New(store, rateWithDelay)\n\tc, err = rateStore.Get(ctx, \"test-rate-object\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Printf(\"Limit: %d\\nReached: %v\\nRemaining: %d\\nReset: %d\\nDelay: %d\\n\", c.Limit, c.Reached, c.Remaining, c.Reset, c.Delay)\n\ttime.Sleep(500 * time.Millisecond) // since it's eventually consistent with redis, you need to give it a sec to sync\n\n}\n\nfunc setupRedisCentralStore(logger *logrus.Entry) *goCache.GenericCache {\n\tos.Setenv(\"NO_REDIS_PASSWORD\", \"true\")\n\t// os.Setenv(\"REDIS_READ_ONLY_ADDRESS\",\"redis://localhost:6379\")\n\t// os.Setenv(\"REDIS_SENTINEL_ADDRESS\", \"redis://localhost:26379\")\n\tselectDatabase := 3\n\tuseSentinel := true\n\tcacheWritePool, err := goCache.InitRedisCache(useSentinel, defExpSeconds, nil, defExpSeconds, defExpSeconds, selectDatabase, logger)\n\tif err != nil {\n\t\tlogrus.Errorf(\"couldn't connect to redis on %s\", redisTestServer)\n\t\tpanic(\"\")\n\t}\n\tlogger.Info(\"cacheWritePool initialized\")\n\treadOnlyPool, err := goCache.InitReadOnlyRedisCache(\"redis://localhost:6379\", \"\", redisOpsTimeout, redisConnTimeout, defExpSeconds, maxConnectionsAllowed, selectDatabase, logger)\n\tif err != nil {\n\t\tlogrus.Errorf(\"couldn't connect to redis on %s\", redisTestServer)\n\t\tpanic(\"\")\n\t}\n\tlogger.Info(\"cacheReadPool initialized\")\n\treturn goCache.NewCacheWithMultiPools(cacheWritePool, readOnlyPool, goCache.L2, sharedSecret, defExpSeconds, []byte(\"test\"), false)\n}\n\n```\nSee also: \n* [examples/generic/main.go](https://github.com/Bose/golimiter/blob/master/examples/generic/main.go)\n\n## Usage with [Gin](https://github.com/gin-gonic/gin)\n\n```go\npackage main\n\nimport (\n\t\"os\"\n\n\t\"github.com/Bose/golimiter\"\n\n\tginlimiter \"github.com/Bose/golimiter/gin\"\n\tginprometheus \"github.com/zsais/go-gin-prometheus\"\n\n\tgoCache \"github.com/Bose/go-cache\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nconst maxConnectionsAllowed = 50 // max connections allowed to the read-only redis cluster from this service\nconst maxEntries = 1000          // max entries allowed in the LRU + expiry in memory store\n\nconst (\n\tredisTestServer       = \"redis://localhost:6379\"\n\tsentinelTestServer    = \"redis://localhost:26379\"\n\tredisMasterIdentifier = \"mymaster\"\n\tsharedSecret          = \"test-secret-must\"\n\tuseSentinel           = true\n\tdefExpSeconds         = 0\n\tredisOpsTimeout       = 50  // millisecods\n\tredisConnTimeout      = 500 // milliseconds\n)\n\nfunc main() {\n\t// use the JSON formatter\n\t// logrus.SetFormatter(\u0026logrus.JSONFormatter{})\n\tlogrus.SetLevel(logrus.DebugLevel)\n\n\tr := gin.Default()\n\tr.Use(gin.Recovery()) // add Recovery middleware\n\tp := ginprometheus.NewPrometheus(\"go_limiter_example\")\n\tp.Use(r)\n\n\tredisCache := setupRedisCentralStore()\n\n\tl, err := ginlimiter.NewLimiter(\n\t\t\"GET::/helloworld\",                     // define the GUID that identifies this rate limiter as GET on the route\n\t\t\"10-2-M\",                               // defines the rate limit based on a std format\n\t\t\"ip-address\",                           // a label that defines the type of rate limiter it is\n\t\tginlimiter.DefaultKeyGetter,            // get the key for this request: IP, request USER identifier, etc\n\t\tginlimiter.DefaultRateExceededReporter, // func that does all the required reporting when a rate is exceeded\n\t\tmaxEntries,                             // max inmemory entries\n\t\t\"constant\",                             // this hint is sent back to the client: constant, exponential, etc\n\t\t1,                                      // primary error code\n\t\t2,                                      // error sub code\n\t\t\"hello-world\",                          // prometheus metric label for this route\n\t\tginlimiter.NewDefaultMetric(\"limtier_example\", \"helloworld\", \"count times helloworld is rate limited\"), // prometheus.ConterVec for rate limit exceeded\n\t\tginlimiter.DefaultMetricIncrementer, // how-to increment the prometheus.CounterVec with the required labels\n\t\tgolimiter.WithUnsyncCounterLimit(1), // use this option to limit how far the mem and central store can drift\n\t\tgolimiter.WithUnsyncTimeLimit(5),    // use this option to update the central store at least every 10ms\n\t)\n\tif err != nil {\n\t\tpanic(\"failed to create limiter \" + err.Error())\n\t}\n\tl.Store.DistributedRedisCache = redisCache\n\n\tl2, err := ginlimiter.NewLimiter(\n\t\t\"GET::/helloworld-delayed\",             // define the GUID that identifies this rate limiter as GET on the route\n\t\t\"10-2-M-1000\",                          // defines the rate limit based on a std format with a delay of 1s\n\t\t\"ip-address\",                           // a label that defines the type of rate limiter it is\n\t\tginlimiter.DefaultKeyGetter,            // get the key for this request: IP, request USER identifier, etc\n\t\tginlimiter.DefaultRateExceededReporter, // func that does all the required reporting when a rate is exceeded\n\t\tmaxEntries,                             // max inmemory entries\n\t\t\"constant\",                             // this hint is sent back to the client: constant, exponential, etc\n\t\t1,                                      // primary error code\n\t\t2,                                      // error sub code\n\t\t\"hello-world\",                          // prometheus metric label for this route\n\t\tginlimiter.NewDefaultMetric(\"limiter_example\", \"helloworld_delayed\", \"count times helloworld is rate limited\"), // prometheus.ConterVec for rate limit exceeded\n\t\tginlimiter.DefaultMetricIncrementer) // how-to increment the prometheus.CounterVec with the required labels\n\tif err != nil {\n\t\tpanic(\"failed to create limiter \" + err.Error())\n\t}\n\tl2.Store.DistributedRedisCache = redisCache\n\n\t// add the rate limiter decorator (which uses a slice of rate limiters)\n\tr.GET(\"/helloworld\", ginlimiter.LimitRoute([]ginlimiter.RateLimiter{l}, func(c *gin.Context) {\n\t\tc.JSON(200, gin.H{\"msg\": \"Hello world!\\n\"})\n\t}))\n\n\t// add the rate limiter decorator (which uses a slice of rate limiters)\n\tr.GET(\"/helloworld-delayed\", ginlimiter.LimitRoute([]ginlimiter.RateLimiter{l2}, func(c *gin.Context) {\n\t\tc.JSON(200, gin.H{\"msg\": \"Hello world!\\n\"})\n\t}))\n\tr.GET(\"nolimits\", func(c *gin.Context) {\n\t\tc.JSON(200, gin.H{\"msg\": \"Hello world!\\n\"})\n\t})\n\n\tif err := r.Run(\":9090\"); err != nil {\n\t\tlogrus.Error(err)\n\t}\n}\n\nfunc setupRedisCentralStore() *goCache.GenericCache {\n\tlogrus.SetLevel(logrus.DebugLevel)\n\tlogger := logrus.WithFields(logrus.Fields{\"method\": \"newInMemoryLimiter\"})\n\n\tlogger.Debug(\"setting envVar TRACE_SYNC= true will turn-on trace logging to the central store for the limiters, you must call this before creating any limiters\")\n\tos.Setenv(\"TRACE_SYNC\", \"true\")\n\n\tos.Setenv(\"NO_REDIS_PASSWORD\", \"true\")\n\t// os.Setenv(\"REDIS_READ_ONLY_ADDRESS\",\"redis://localhost:6379\")\n\t// os.Setenv(\"REDIS_SENTINEL_ADDRESS\", \"redis://localhost:26379\")\n\tselectDatabase := 3\n\tuseSentinel := true\n\tcacheWritePool, err := goCache.InitRedisCache(useSentinel, defExpSeconds, nil, defExpSeconds, defExpSeconds, selectDatabase, logger)\n\tif err != nil {\n\t\tlogrus.Errorf(\"couldn't connect to redis on %s\", redisTestServer)\n\t\tpanic(\"\")\n\t}\n\tlogger.Info(\"cacheWritePool initialized\")\n\treadOnlyPool, err := goCache.InitReadOnlyRedisCache(redisTestServer, \"\", redisOpsTimeout, redisConnTimeout, defExpSeconds, maxConnectionsAllowed, selectDatabase, logger)\n\tif err != nil {\n\t\tlogrus.Errorf(\"couldn't connect to redis on %s\", redisTestServer)\n\t\tpanic(\"\")\n\t}\n\tlogger.Info(\"cacheReadPool initialized\")\n\treturn goCache.NewCacheWithMultiPools(cacheWritePool, readOnlyPool, goCache.L2, sharedSecret, defExpSeconds, []byte(\"test\"), false)\n}\n\n```\n\nSee also: \n* [examples/gin/example.go](https://github.com/Bose/golimiter/blob/master/examples/gin/example.go)\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbose%2Fgolimiter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbose%2Fgolimiter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbose%2Fgolimiter/lists"}