{"id":18656558,"url":"https://github.com/zendesk/prop","last_synced_at":"2025-10-04T11:15:00.979Z","repository":{"id":982119,"uuid":"784411","full_name":"zendesk/prop","owner":"zendesk","description":"Puts a cork in their requests","archived":false,"fork":false,"pushed_at":"2025-03-17T15:19:14.000Z","size":265,"stargazers_count":121,"open_issues_count":5,"forks_count":13,"subscribers_count":420,"default_branch":"main","last_synced_at":"2025-03-31T19:11:06.681Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zendesk.png","metadata":{"files":{"readme":"README.md","changelog":"Changelog.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2010-07-19T15:50:08.000Z","updated_at":"2025-01-07T15:18:44.000Z","dependencies_parsed_at":"2023-07-07T23:46:41.138Z","dependency_job_id":"b50a4b14-cd20-49f1-87b4-cc19547ccef8","html_url":"https://github.com/zendesk/prop","commit_stats":{"total_commits":263,"total_committers":28,"mean_commits":9.392857142857142,"dds":0.5627376425855513,"last_synced_commit":"f54ececfa3237ab07b85da3e9e089d31dc0fac9a"},"previous_names":[],"tags_count":47,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zendesk%2Fprop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zendesk%2Fprop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zendesk%2Fprop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zendesk%2Fprop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zendesk","download_url":"https://codeload.github.com/zendesk/prop/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247721898,"owners_count":20985084,"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":[],"created_at":"2024-11-07T07:23:59.862Z","updated_at":"2025-10-04T11:15:00.948Z","avatar_url":"https://github.com/zendesk.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Prop ![Build status](https://github.com/zendesk/prop/workflows/ci/badge.svg)\n\nA gem to rate limit requests/actions of any kind.\u003cbr/\u003e\nDefine thresholds, register usage and finally act on exceptions once thresholds get exceeded.\n\nProp supports two limiting strategies:\n\n* Basic strategy (default): Prop will use an interval to define a window of time using simple div arithmetic. \nThis means that it's a worst-case throttle that will allow up to two times the specified requests within the specified interval.\n* Leaky bucket strategy: Prop also supports the [Leaky Bucket](https://en.wikipedia.org/wiki/Leaky_bucket) algorithm, \nwhich is similar to the basic strategy but also supports bursts up to a specified threshold.\n\nTo store values, prop needs a cache:\n\n```ruby\n# config/initializers/prop.rb\nProp.cache = Rails.cache # needs read/write/increment methods\n```\n\nWhen using the interval strategy, prop sets a key expiry to its interval.  Because the leaky bucket strategy does not set a ttl, it is best to use memcached or similar for all prop caching, not redis.\n\n## Setting a Callback\n\nYou can define an optional callback that is invoked when a rate limit is reached. In a Rails application you \ncould use such a handler to add notification support:\n\n```ruby\nProp.before_throttle do |handle, key, threshold, interval|\n  ActiveSupport::Notifications.instrument('throttle.prop', handle: handle, key: key, threshold: threshold, interval: interval)\nend\n```\n\n## Setting an After Evaluated Callback\n\nYou can define an optional callback that is invoked when a rate limit is checked. The callback will be invoked regardless\nof the result of the evaluation.\n\n```ruby\nProp.after_evaluated do |handle, counter, options|\n  Rails.logger.info \"Prop #{handle} has just been check. current value: #{counter}\"\nend\n```\n\n## Defining thresholds\n\nExample: Limit on accepted emails per hour from a given user, by defining a threshold and interval (in seconds):\n\n```ruby\nProp.configure(:mails_per_hour, threshold: 100, interval: 1.hour, description: \"Mail rate limit exceeded\")\n\n# Block requests by setting threshold to 0\nProp.configure(:mails_per_hour, threshold: 0, interval: 1.hour, description: \"All mail is blocked\")\n```\n\n```ruby\n# Throws Prop::RateLimited if the threshold/interval has been reached\nProp.throttle!(:mails_per_hour)\n\n# Prop can be used to guard a block of code\nProp.throttle!(:expensive_request) { calculator.something_very_hard }\n\n# Returns true if the threshold/interval has been reached\nProp.throttled?(:mails_per_hour)\n\n# Sets the throttle count to 0\nProp.reset(:mails_per_hour)\n\n# Returns the value of this throttle, usually a count, but see below for more\nProp.count(:mails_per_hour)\n```\n\nProp will raise a `KeyError` if you attempt to operate on an undefined handle.\n\n## Scoping a throttle\n\nExample: scope the throttling to a specific sender rather than running a global \"mails per hour\" throttle:\n\n```ruby\nProp.throttle!(:mails_per_hour, mail.from)\nProp.throttled?(:mails_per_hour, mail.from)\nProp.reset(:mails_per_hour, mail.from)\nProp.query(:mails_per_hour, mail.from)\n```\n\nThe throttle scope can also be an array of values:\n\n```ruby\nProp.throttle!(:mails_per_hour, [ account.id, mail.from ])\n```\n\n## Error handling\n\nIf the threshold for a given handle and key combination is exceeded, Prop throws a `Prop::RateLimited`. \nThis exception contains a \"handle\" reference and a \"description\" if specified during the configuration. \nThe handle allows you to rescue `Prop::RateLimited` and differentiate action depending on the handle. \nFor example, in Rails you can use this in e.g. `ApplicationController`:\n\n```ruby\nrescue_from Prop::RateLimited do |e|\n  if e.handle == :authorization_attempt\n    render status: :forbidden, message: I18n.t(e.description)\n  elsif ...\n\n  end\nend\n```\n\n### Using the Middleware\n\nProp ships with a built-in Rack middleware that you can use to do all the exception handling. \nWhen a `Prop::RateLimited` error is caught, it will build an HTTP \n[429 Too Many Requests](http://tools.ietf.org/html/draft-nottingham-http-new-status-02#section-4) \nresponse and set the following headers:\n\n    Retry-After: 32\n    Content-Type: text/plain\n    Content-Length: 72\n\nWhere `Retry-After` is the number of seconds the client has to wait before retrying this end point. \nThe body of this response is whatever description Prop has configured for the throttle that got violated, \nor a default string if there's none configured.\n\nIf you wish to do manual error messaging in these cases, you can define an error handler in your Prop configuration. \nHere's how the default error handler looks - you use anything that responds to `.call` and \ntakes the environment and a `RateLimited` instance as argument:\n\n```ruby\nerror_handler = Proc.new do |env, error|\n  body    = error.description || \"This action has been rate limited\"\n  headers = { \"Content-Type\" =\u003e \"text/plain\", \"Content-Length\" =\u003e body.size, \"Retry-After\" =\u003e error.retry_after }\n\n  [ 429, headers, [ body ]]\nend\n\nActionController::Dispatcher.middleware.insert_before(ActionController::ParamsParser, error_handler: error_handler)\n```\n\nAn alternative to this, is to extend `Prop::Middleware` and override the `render_response(env, error)` method.\n\n## Disabling Prop\n\nIn case you need to perform e.g. a manual bulk operation:\n\n```ruby\nProp.disabled do\n  # No throttles will be tested here\nend\n```\n\n## Overriding threshold\n\nYou can chose to override the threshold for a given key:\n\n```ruby\nProp.throttle!(:mails_per_hour, mail.from, threshold: current_account.mail_throttle_threshold)\n```\n\nWhen `throttle` is invoked without argument, the key is nil and as such a scope of its own, i.e. these are equivalent:\n\n```ruby\nProp.throttle!(:mails_per_hour)\nProp.throttle!(:mails_per_hour, nil)\n```\n\nThe default (and smallest possible) increment is 1, you can set that to any integer value using \n`:increment` which is handy for building time based throttles:\n\n```ruby\nProp.configure(:execute_time, threshold: 10, interval: 1.minute)\nProp.throttle!(:execute_time, account.id, increment: (Benchmark.realtime { execute }).to_i)\n```\n\nDecrement can be used to for example throttle before an expensive action and then give quota back when some condition is met.\n\n```ruby\nProp.throttle!(:api_counts, request.remote_ip, decrement: 1)\n```\n\n## Optional configuration\n\nYou can add optional configuration to a prop and retrieve it using `Prop.configurations[:foo]`:\n\n```ruby\nProp.configure(:api_query, threshold: 10, interval: 1.minute, category: :api)\nProp.configure(:api_insert, threshold: 50, interval: 1.minute, category: :api)\nProp.configure(:password_failure, threshold: 5, interval: 1.minute, category: :auth)\n```\n\n```\nProp.configurations[:api_query][:category]\n```\n\nYou can use `Prop::RateLimited#config` to distinguish between errors:\n\n```ruby\nrescue Prop::RateLimited =\u003e e\n  case e.config[:category]\n  when :api\n    raise APIRateLimit\n  when :auth\n    raise AuthFailure\n  ...\nend\n```\n\n## First throttled\n\nYou can opt to be notified when the throttle is breached for the first time.\u003cbr/\u003e\nThis can be used to send notifications on breaches but prevent spam on multiple throttle breaches.\n\n```Ruby\nProp.configure(:mails_per_hour, threshold: 100, interval: 1.hour, first_throttled: true)\n\nthrottled = Prop.throttle(:mails_per_hour, user.id, increment: 60)\nif throttled\n  if throttled == :first_throttled\n    ApplicationMailer.spammer_warning(user).deliver_now\n  end\n  Rails.logger.warn(\"Not sending emails\")\nelse\n  send_emails\nend\n\n# return values of throttle are: false, :first_throttled, true\n\nProp.first_throttled(:mails_per_hour, 1, increment: 60) # -\u003e false\nProp.first_throttled(:mails_per_hour, 1, increment: 60) # -\u003e :first_throttled\nProp.first_throttled(:mails_per_hour, 1, increment: 60) # -\u003e true\n\n# can also be accesses on `Prop::RateLimited` exceptions as `.first_throttled` \n```\n\n## Using Leaky Bucket Algorithm\n\nYou can add two additional configurations: `:strategy` and `:burst_rate` to use the \n[leaky bucket algorithm](https://en.wikipedia.org/wiki/Leaky_bucket). \nProp will handle the details after configured, and you don't have to specify `:strategy` \nagain when using `throttle`, `throttle!` or any other methods.\n\nThe leaky bucket algorithm used is \"leaky bucket as a meter\".\n\n```ruby\nProp.configure(:api_request, strategy: :leaky_bucket, burst_rate: 20, threshold: 5, interval: 1.minute)\n```\n\n* `:threshold` value here would be the \"leak rate\" of leaky bucket algorithm.\n\n### Releasing a new version\nA new version is published to RubyGems.org every time a change to `version.rb` is pushed to the `main` branch.\nIn short, follow these steps:\n1. Update `version.rb`,\n2. update version in all `Gemfile.lock` files,\n3. merge this change into `main`, and\n4. look at [the action](https://github.com/zendesk/prop/actions/workflows/publish.yml) for output.\n\nTo create a pre-release from a non-main branch:\n1. change the version in `version.rb` to something like `1.2.0.pre.1` or `2.0.0.beta.2`,\n2. push this change to your branch,\n3. go to [Actions → “Publish to RubyGems.org” on GitHub](https://github.com/zendesk/prop/actions/workflows/publish.yml),\n4. click the “Run workflow” button,\n5. pick your branch from a dropdown.\n\n## License\n\nCopyright 2015 Zendesk\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, \nsoftware distributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. \nSee the License for the specific language governing permissions and limitations under the License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzendesk%2Fprop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzendesk%2Fprop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzendesk%2Fprop/lists"}