-
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
178: Whiteboard Practice: Implementing a Rate Limiter Algorithm
When you hit a whiteboard interview and they ask you to implement a rate limiter, they aren't just checking if you know the algorithm. They're looking at how you handle state over time and how you deal with edge cases—like "burstiness."
I've always preferred the Token Bucket algorithm for these discussions. It's intuitive: imagine a bucket that holds a maximum number of tokens. Every request costs one token. If the bucket is empty, the request is rejected. Meanwhile, tokens drip back into the bucket at a steady rate. It allows for a bit of "burst" traffic while keeping the average rate in check.
Defining the Bucket's State
First, I need to decide what information I actually need to track. To make this work, I don't want a background timer constantly adding tokens (that would be a nightmare for performance). Instead, I'll calculate how many tokens should have been added since the last time a request came in.
class RateLimiter
def initialize(max_tokens, refill_rate)
@max_tokens = max_tokens
@refill_rate = refill_rate # tokens per second
@tokens = max_tokens
@last_refill_time = Time.now
end
end
I'm setting the initial state to be "full." In a real-world scenario, you'd probably associate this instance with a specific User ID or IP address in a Redis store, but for the whiteboard, a simple class will do.
Calculating the Refill Logic
Now, I need a method that checks if a request can pass. This is where the "lazy refill" happens. I'll calculate the time elapsed, multiply it by the rate, and add that to the current token count.
def allow_request?
now = Time.now
elapsed = now - @last_refill_time
# Refill the bucket based on time passed
@tokens += elapsed * @refill_rate
@last_refill_time = now
if @tokens >= 1
@tokens -= 1
return true
end
false
end
Catching the "Infinite Token" Bug
Wait. If I leave the code like this, I've made a classic mistake. If a user doesn't make a request for an hour, elapsed * @refill_rate will be a huge number. My @tokens count will grow to thousands, effectively allowing the user to blast the API with thousands of requests instantly, completely bypassing the "rate" part of the rate limiter.
I need to cap the tokens at @max_tokens. This ensures that while we allow a small burst (the size of the bucket), we don't allow an infinite accumulation of credit.
def allow_request?
now = Time.now
elapsed = now - @last_refill_time
# Fix: Cap the tokens so they don't grow indefinitely
@tokens = [@max_tokens, @tokens + (elapsed * @refill_rate)].min
@last_refill_time = now
if @tokens >= 1
@tokens -= 1
return true
end
false
end
Putting it Together for a Test Drive
To make sure this actually behaves as expected, I'll simulate a few requests. I'll set a limit of 5 tokens, refilling at 1 token per second. This means I can burst 5 requests immediately, but then I'm limited to one per second.
limiter = RateLimiter.new(5, 1)
# Burst 6 requests
6.times do |i|
puts "Request #{i+1}: #{limiter.allow_request? ? 'Allowed' : 'Rejected'}"
end
# Wait 2 seconds and try again
sleep 2
puts "Request 7 (after 2s): #{limiter.allow_request? ? 'Allowed' : 'Rejected'}"
If you run this, you'll see the first 5 pass, the 6th fail, and the 7th pass. It's a simple pattern, but the "cap" logic is usually what separates a junior implementation from a senior one in an interview setting.
📋 Practical Task
Exercise: Implement a Tiered Rate Limiter
In the real world, different users have different limits (e.g., "Free" vs "Premium" tiers). Your task is to modify the RateLimiter logic to support tiered configurations.
Build a class called TieredRateLimiter that takes a user_tier symbol (:free or :premium) during initialization. Use the following configurations:
- Free: Max 3 tokens, refills at 0.5 tokens per second.
- Premium: Max 10 tokens, refills at 2 tokens per second.
Your implementation should:
- Assign the correct
max_tokensandrefill_ratebased on the tier. - Maintain the "cap" logic so tokens do not exceed the tier's maximum.
- Implement the
allow_request?method to return true or false.
There are no comments for now.