CurbIt

CurbIt makes it easy to add application level rate limiting to your Rails app by using a controller macro.

CurbIt is NOT a replacement for properly configured rate limiting at fronting services. Properly defending your app against DoS attacks or other malicious client behavior should always include configuring rate limiting and throttling at the services that sit in front of your app. This includes firewalls, load balancers, and reverse proxies. But sometimes that just isn’t enough. Sometimes you want to rate limit requests from users based on application logic that is not practical to get to or replicate in those services.

Usage

Minimal configuration

Quick setup inside your Rails controller. ActionController::Base is already extended to include Curbit when installed as a plugin. This will add a rate_limit “macro” to your controllers.

class InvitesController < ApplicationController
  def invite
    # invite logic...
  end

  rate_limit :invite, :max_calls => 2, :time_limit => 30.seconds, :wait_time => 1.minute
end

If a user calls the invite service from the same remote address more than 2 times within 30 seconds, CurbIt will render a ‘503 Service Unavailable’ response and the invite method is never called. The user will then need to wait 1 minute before being allowed to make the request again. Default response messages for html, xml, and json formats are rendered as required.

Custom client identifier

If you don’t want to use the remote address to identify the client, you can specify a method that CurbIt will call to get a key from.

class InvitesController < ApplicationController
  def invite
    # invite logic...
  end

  rate_limit :invite, :key => :userid, :max_calls => 2,
                      :time_limit => 30.seconds, :wait_time => 1.minute
  def userid
    session[:user_id]
  end
end

CurbIt will call the :userid method and use the returned value to create a unique identifier. This identifier is used to index cached information about the request.

You can alternatively pass a Proc that will take the controller instance as an argument.

rate_limit :invite, :key => proc {|c| c.session[:user_id]},
                    :max_calls => 2,
                    :time_limit => 30.seconds, :wait_time => 1.minute

(If you’re wondering why CurbIt passes the controller into the proc, it’s because the Proc is not bound to the controller instance when it’s defined. This way, you can at least have access to stuff you might need.)

Custom message

You might like to customize the messages returned by CurbIt.

class InvitesController < ApplicationController
  def invite
    # invite logic...
  end

  rate_limit :invite, :max_calls => 2, :time_limit => 30.seconds, :wait_time => 1.minute,
                      :message => "Hey! Slow down there cow polk.",
                      :status => 200
end

After reaching the maximum threshold of requests, CurbIt will render the message “Hey! Slow down there cow polk.” with a response status of 200. If :status is not defined, CurbIt will set a 503 status on the response. CurbIt will also embed this message into some default json or xml containers based on the request format.

Custom message rendering

CurbIt does it’s best to render a response based on the requested format, but you might have an obscure mime-type you’re using or you might like to customize the response rendering.

class InvitesController < ApplicationController
  def invite
    # invite logic...
  end

  rate_limit :invite, :max_calls => 2, :time_limit => 30.seconds, :wait_time => 1.minute,
                      :message => :limit_response

  def limit_response(wait_time)
    respond_to {|fmt|
      fmt.csv {
        render :text => "Plese wait #{wait_time} seconds before trying again",
               :status => 200
      }
    }
  end

end

Here, CurbIt will relinquish all control for response rendering to your method. This will ignore any :status argument set in the cofig options.

Default response bodies

  • xml: <error>message</error>

  • json: “error”:{“message”}

  • html: message

  • text: message

Rails Installation

As a Gem

Gem install

$ gem install curbit --source http://gemcutter.org

Rails dependency

Specify the gem dependency in your config/environment.rb file:

Rails::Initializer.run do |config|
  #...
  config.gem "curbit", :source => "http://gemcutter.org
  #...
end

Then:

$ rake gems:install
$ rake gems:unpack

Then include in your controllers:

class MyController < ApplicationController
  include Curbit::Controller
  #...
end

As a Plugin

$ script/plugin install git://github.com/ssayles/curbit.git

Curbit::Controller will automatically be added to your controllers so you can call the rate_limit macro whenever you want.

Dependencies

CurbIt utilizes Rails.cache to store information about requests. Be mindful that if you are running a cluster, you’ll want to make sure you’re using a shared cache like memcached.

That’s it!

Credits

CurbIt is written and maintained by Scott Sayles.

Copyright

Copyright © 2009 Scott Sayles. See LICENSE for details.