Skip to Content
Course content

126: Rate Limiting with Rack::Attack

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen more times than I'd like to admit. A developer decides to protect their login endpoint from brute-force attacks, installs rack-attack, and writes what looks like perfectly reasonable code. Then, they deploy it, and suddenly, the entire engineering team is getting 429 "Too Many Requests" errors, even though they're all on different machines.

# config/initializers/rack_attack.rb
Rack::Attack.throttle('logins/ip', limit: 5, period: 20.seconds) do |req|
  if req.path == '/login' && req.post?
    'login_attempt'
  end
end

The "Global Block" Blunder

If you look closely at that block, you'll see the mistake. The throttle method expects the block to return a unique identifier for the client being throttled. In the example above, I'm returning the string 'login_attempt' for every single person who hits the login page.

To Rack::Attack, this looks like one giant user is making all the requests. Once the 5th request hits the server—regardless of whether it came from your laptop in New York or your colleague's laptop in London—the limit is reached, and everyone gets locked out. It's a classic "global throttle" by accident.

Making the Throttle Dynamic

To fix this, we need to return something unique to the requester. Usually, that's the IP address. By changing the return value to req.ip, Rack::Attack creates a separate counter in the cache for every unique IP address.

# config/initializers/rack_attack.rb
Rack::Attack.throttle('logins/ip', limit: 5, period: 20.seconds) do |req|
  if req.path == '/login' && req.post?
    req.ip # Now we track each IP individually
  end
end

Now, if a bot tries to hammer your login page from one IP, only that IP gets blocked. Your team can keep working, and your legitimate users aren't affected by a single bad actor.

Connecting the Cache Store

Here is a detail that often trips people up: Rack::Attack doesn't have its own database. It relies on Rails.cache to remember how many requests an IP has made. If you're in development and using the default :null_store, your rate limits will simply never trigger. You'll be hammering the endpoint and wondering why the middleware is ignoring you.

I always recommend using Redis for this in production because it's fast and handles expiration automatically. In your config/application.rb or environment files, make sure you have a functioning cache store:

config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }

Customizing the "Too Many Requests" Response

By default, Rack::Attack returns a plain text "Retry later" message. That's fine for a CLI tool, but for a real app, it's a jarring user experience. You can override the throttled_responder to return a proper JSON response or a branded HTML page.

I usually prefer JSON for APIs so the frontend can actually catch the 429 status and show a "Please wait 30 seconds" message to the user instead of a blank white screen.

Rack::Attack.throttled_responder = lambda do |request|
  [ 429,  # status
    { 'Content-Type' => 'application/json' }, # headers
    [{ error: 'Slow down! You are making too many requests.' }.to_json] # body
  ]
end

One last tip: always remember to whitelist your own internal health-check IPs or monitoring services. There's nothing quite as stressful as waking up to a production outage because your own uptime monitor accidentally triggered a rate limit and blocked itself.




📋 Practical Task

Build a Tiered API Rate Limiter

You are building a weather API. You want to implement Rack::Attack to support two tiers of users: Free and Premium. Free users should be limited to 10 requests per minute, while Premium users get 100 requests per minute.

Requirements:

  • Create a throttle that checks for an X-API-KEY header in the request.
  • If the header value is 'premium_secret_key', allow up to 100 requests per minute.
  • For any other API key (or no key at all), limit the request to 10 per minute based on the IP address.
  • Implement a custom throttled_responder that returns a JSON object containing the message: "API limit exceeded. Upgrade to Premium for more requests."

Test your implementation by hitting the endpoint with different headers and verifying that the Premium key allows significantly more traffic than the default IP-based limit.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.