Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
126: Rate Limiting with Rack::Attack
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-KEYheader 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_responderthat 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.
There are no comments for now.