{"id":13491307,"url":"https://github.com/grempe/ex_rated","last_synced_at":"2026-02-21T17:34:25.638Z","repository":{"id":19789905,"uuid":"23049155","full_name":"grempe/ex_rated","owner":"grempe","description":"ExRated, the Elixir OTP GenServer with the naughty name that allows you to rate-limit calls to any service that requires it.","archived":false,"fork":false,"pushed_at":"2023-03-30T14:01:42.000Z","size":141,"stargazers_count":459,"open_issues_count":4,"forks_count":51,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-10-24T05:55:33.518Z","etag":null,"topics":["api","bucket","elixir","otp","phoenix","rate","rate-limit","rate-limiting","rate-limits"],"latest_commit_sha":null,"homepage":"","language":"Elixir","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/grempe.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}},"created_at":"2014-08-17T20:00:03.000Z","updated_at":"2025-08-17T02:35:16.000Z","dependencies_parsed_at":"2024-01-31T07:13:16.786Z","dependency_job_id":"c8e2cda7-f983-4c1e-933b-ec91ff80db70","html_url":"https://github.com/grempe/ex_rated","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/grempe/ex_rated","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grempe%2Fex_rated","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grempe%2Fex_rated/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grempe%2Fex_rated/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grempe%2Fex_rated/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/grempe","download_url":"https://codeload.github.com/grempe/ex_rated/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grempe%2Fex_rated/sbom","scorecard":{"id":445263,"data":{"date":"2025-08-18","repo":{"name":"github.com/grempe/ex_rated","commit":"bc67320c2adfa92bcda7837a7d25723d6671024b"},"scorecard":{"version":"v5.2.1-41-g40576783","commit":"40576783fda6698350fcbbeaea760ff827433034"},"score":4.1,"checks":[{"name":"Code-Review","score":3,"reason":"Found 8/21 approved changesets -- score normalized to 3","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#code-review"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#dangerous-workflow"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#maintained"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#packaging"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#binary-artifacts"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/elixir.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/elixir.yml:70: update your workflow using https://app.stepsecurity.io/secureworkflow/grempe/ex_rated/elixir.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/elixir.yml:71: update your workflow using https://app.stepsecurity.io/secureworkflow/grempe/ex_rated/elixir.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/elixir.yml:86: update your workflow using https://app.stepsecurity.io/secureworkflow/grempe/ex_rated/elixir.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/elixir.yml:87: update your workflow using https://app.stepsecurity.io/secureworkflow/grempe/ex_rated/elixir.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/elixir.yml:102: update your workflow using https://app.stepsecurity.io/secureworkflow/grempe/ex_rated/elixir.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/elixir.yml:103: update your workflow using https://app.stepsecurity.io/secureworkflow/grempe/ex_rated/elixir.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/elixir.yml:118: update your workflow using https://app.stepsecurity.io/secureworkflow/grempe/ex_rated/elixir.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/elixir.yml:119: update your workflow using https://app.stepsecurity.io/secureworkflow/grempe/ex_rated/elixir.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/elixir.yml:20: update your workflow using https://app.stepsecurity.io/secureworkflow/grempe/ex_rated/elixir.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/elixir.yml:21: update your workflow using https://app.stepsecurity.io/secureworkflow/grempe/ex_rated/elixir.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/elixir.yml:38: update your workflow using https://app.stepsecurity.io/secureworkflow/grempe/ex_rated/elixir.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/elixir.yml:39: update your workflow using https://app.stepsecurity.io/secureworkflow/grempe/ex_rated/elixir.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/elixir.yml:54: update your workflow using https://app.stepsecurity.io/secureworkflow/grempe/ex_rated/elixir.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/elixir.yml:55: update your workflow using https://app.stepsecurity.io/secureworkflow/grempe/ex_rated/elixir.yml/master?enable=pin","Info:   0 out of   7 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   7 third-party GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#cii-best-practices"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#fuzzing"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#security-policy"}},{"name":"License","score":9,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Warn: project license file does not contain an FSF or OSI license."],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#license"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#branch-protection"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#signed-releases"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#vulnerabilities"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 18 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-19T06:33:55.034Z","repository_id":19789905,"created_at":"2025-08-19T06:33:55.035Z","updated_at":"2025-08-19T06:33:55.035Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29688273,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-21T15:51:39.154Z","status":"ssl_error","status_checked_at":"2026-02-21T15:49:03.425Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: 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":["api","bucket","elixir","otp","phoenix","rate","rate-limit","rate-limiting","rate-limits"],"created_at":"2024-07-31T19:00:55.497Z","updated_at":"2026-02-21T17:34:20.628Z","avatar_url":"https://github.com/grempe.png","language":"Elixir","funding_links":[],"categories":["Elixir","Miscellaneous"],"sub_categories":[],"readme":"# ExRated\n\nExRated is:\n\n1. A port of the Erlang '[raterlimiter](https://github.com/Gromina/raterlimiter)' project to Elixir.\n2. An OTP GenServer process that allows you to rate limit calls to something like an external API.\n3. The Hex.pm package with the naughty name.\n\nYou can learn more about the concept for this rate limiter in [the Token Bucket article on Wikipedia](http://en.wikipedia.org/wiki/Token_bucket)\n\nIf you use the PhoenixFramework there is also a great blog post on [Rate Limiting a Phoenix API](https://blog.danielberkompas.com/2015/06/16/rate-limiting-a-phoenix-api) by [danielberkompas](https://github.com/danielberkompas) describing how to write a plug\nto use ExRated in your own API. Its fast and its easy.\n\n## Usage\n\nCall the ExRated application with `ExRated.check_rate/3`. This function takes three arguments:\n\n1. A `bucket name` (Erlang term, typically String). You can have as many buckets as you need.\n2. A `scale` (Integer). The time scale in milliseconds that the bucket is valid for.\n3. A `limit` (Integer). How many actions you want to limit your app to in the time scale provided.\n\nFor example, if you have to enforce a rate limit of no more than 5 calls in 10 seconds to your API:\n\n```elixir\niex\u003e ExRated.check_rate(\"my-rate-limited-api\", 10_000, 5)\n{:ok, 1}\n```\n\nThe `ExRated.check_rate` function will return an `{:ok, Integer}` tuple if its OK to proceed with your rate limited function. The Integer returned is the current value of the incrementing counter showing how many times in the time scale window your function has already been called. If you are over limit a `{:error, Integer}` tuple will be returned where the Integer is always the limit you have specified in the function call.\n\nCall the ExRated application with `ExRated.inspect_bucket/3`. This function takes the same three arguments as `check_rate`:\n\nFor example, if you want to inspect the bucket for your API:\n\n```elixir\niex\u003e ExRated.inspect_bucket(\"my-rate-limited-api\", 10_000, 5)\n{0, 5, 2483, nil, nil}\niex\u003e ExRated.check_rate(\"my-rate-limited-api\", 10_000, 5)\n{:ok, 1}\niex\u003e ExRated.inspect_bucket(\"my-rate-limited-api\", 10_000, 5)\n{1, 4, 723, 1450282268397, 1450282268397}\n```\n\nThe `ExRated.inspect_bucket` function will return a `{count, count_remaining, ms_to_next_bucket, created_at, updated_at}` tuple, count and count_remaining are integers, ms_to_next_bucket is the number of milliseconds before the bucket resets, created_at and updated_at are timestamps in milliseconds.\n\nCall the ExRated application with `ExRated.delete_bucket/1`. This function takes one argument:\n\n1. A `bucket name` (String). You can have as many buckets as you need.\n\nFor example, if you want to reset the counter for your API:\n\n```elixir\niex\u003e ExRated.delete_bucket(\"my-rate-limited-api\")\n:ok\n```\n\nThe `ExRated.delete_bucket` function will return an `:ok` on success or `:error` if the bucket doesn't exist\n\n## Installation\n\nYou can use ExRated in your projects in two steps:\n\n1. Add ExRated to your `mix.exs` dependencies:\n\n   ```elixir\n   def deps do\n     [{:ex_rated, \"~\u003e 2.0\"}]\n   end\n   ```\n\n2. List `:ex_rated` in your application dependencies:\n\n   ```elixir\n   def application do\n     [applications: [:ex_rated]]\n   end\n   ```\n\nYou can also start the GenServer manually, and pass it custom config, with something like:\n\n```elixir\n{:ok, pid} = GenServer.start_link(ExRated, [ {:timeout, 10_000}, {:cleanup_rate, 10_000}, {:persistent, false} ], [name: :ex_rated])\n```\n\nAlternatively, you can configure them in your `config/config.exs` (or\nother config) file like\n\n```elixir\nconfig :ex_rated,\n  timeout: 10_000,\n  cleanup_rate: 10_000,\n  persistent: false,\n  name: :ex_rated,\n  ets_table_name: :ets_rated_test_buckets\n```\n\nThese args and their defaults are:\n\n`{:timeout, 90_000_000}` : buckets older than this in milliseconds will be automatically pruned.\n\n`{:cleanup_rate, 60_000}` : how often, in milliseconds, the bucket pruning process will be run.\n\n`{:ets_table_name, :ex_rated_buckets}` : The atom name of the ETS\ntable. This can be configured within your `config` files but not when\nstarting the GenServer manually.\n\n`{:persistent, false}` : Whether to persist ETS table to disk with DETS on server stop/restart.\n\n`[name: :ex_rated]` : The registered name of the ExRated GenServer.\n\n## Contributing\n\nPlease run the following commands before pushing a pull request to ensure your code has been properly formatted and static analysis run.\n\n```sh\nmix format\nmix credo --strict\n```\n\n## Testing\n\nIt is important that the OTP doesn't get automatically started by Mix.\n\n```elixir\nmix test --no-start\n```\n\n## Is it fast?\n\nYou can use the `Benchfella` library to do a quick performance test.\n\nOn a 2019 Macbook Pro (2.3 GHz 8-Core Intel Core i9, 64GB RAM) the lib can do 10,000,000 checks in less than 10s, averaging 0.89 µs/op (microseconds).\n\n```text\n$ mix bench\nCompiling 1 file (.ex)\nSettings:\n  duration:      1.0 s\n\n## BasicBench\n[10:45:54] 1/1: Basic Bench\n\nFinished in 9.87 seconds\n\n## BasicBench\nbenchmark na iterations   average time\nBasic Bench    10000000   0.89 µs/op\n```\n\n## Changes\n\n### v2.1.0\n\n- Fix compilation issues with latest Elixir\n- Update dependencies\n- Update test targets, dropping 1.6\n\n### v2.0.1\n\n- Add matrix tests for Elixir/OTP versions for Elixir \u003e= 1.6. [@jechol]\n- Remove deprecated `Supervisor.Spec.worker` usage. [@jechol]\n- Remove `init/1` from generated docs. [@jechol]\n- Update config example in README.md\n\n### v2.0.0\n\n- [BREAKING] Fixes #24 (Avoid GenServer Serialization) [@nabaskes, @benwilson512]\n  - Improves performance from 2.26 µs/op to 0.89 µs/op (same hardware)\n  - Breaking due to changed method for configuring `ets_table_name` if overriding.\n- Bucket names can be any Erlang term. Fixes #17 [@denvera]\n- Update `ex_doc` and `ex2ms` dependencies.\n- `_` prefix unused variables to avoid compilation warnings.\n- Fix compilation warning with `ets_table_name()`\n- Added GitHub Elixir test action.\n\n### v1.3.3\n\n- Eliminate `warning: function timestamp/1 is unused`. [@brianberlin]\n\n### v1.3.2\n\n- Automatic application inference\n- Update ex_doc dependency\n- Update minimum Elixer to 1.6+\n- Update \"Rate Limiting\" blogpost URL reference\n\n### v1.3.1\n\n- Update `ex2ms` to v1.5\n\n### v1.3.0\n\n- Fix compilation warnings. [@walkr]\n- Start app properly with no args [@walkr]\n- Modify start_link to be callable by `Supervisor.Spec.worker` fun [@walkr]\n\n### v1.2.2\n\n- Update Elixir to v1.2\n- Update `ex2ms` to v1.4\n\n### v1.2.1\n\n- Change ETS Table to private.\n- Change ETS table name to a non-test name.\n\n### v1.2.0\n\n- Added `{:persistent, false}` option to server config to allow persisting data to disk.\n- Fixed minor compilation warning.\n\n### v1.1.0\n\n- Added `delete_bucket/1` function. Takes a bucket name and removes it now instead of waiting for pruning (Nick Sanders).\n- Added `inspect_bucket/3` function. Returns metadata about buckets (Nick Sanders).\n\n### v1.0.0\n\n- [BREAKING] Return {:error, limit} instead of {:fail, limit} to be a bit more idiomatic. Requires semver major version number change.\n- Support Elixir version 1.1 in addition to 1.0\n\n### v0.0.6\n\n- ExRated internally calls `:erlang.system_time(:milli_seconds)` provided by the new [Time API in OTP 18](http://www.erlang.org/doc/apps/erts/time_correction.html) and greater if available, and will fall back gracefully to the old `:erlang.now()` in older versions. Thanks to Mitchell Henke (mitchellhenke) for the enhancement.\n\n## License\n\nExRated source code is released under Apache 2 License.\nCheck LICENSE file for more information.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrempe%2Fex_rated","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgrempe%2Fex_rated","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrempe%2Fex_rated/lists"}