Class: SecApi::Middleware::RateLimiter
- Inherits:
-
Faraday::Middleware
- Object
- Faraday::Middleware
- SecApi::Middleware::RateLimiter
- Defined in:
- lib/sec_api/middleware/rate_limiter.rb
Overview
Faraday middleware that extracts rate limit headers from API responses, proactively throttles requests when approaching the rate limit, and queues requests when the rate limit is exhausted.
This middleware parses X-RateLimit-* headers from sec-api.io responses and updates a shared state store with the current rate limit information. When the remaining quota drops below a configurable threshold, the middleware will sleep until the rate limit window resets to avoid hitting 429 errors. When remaining reaches 0 (exhausted), requests are queued until reset.
Headers parsed:
-
X-RateLimit-Limit: Total requests allowed per time window
-
X-RateLimit-Remaining: Requests remaining in current window
-
X-RateLimit-Reset: Unix timestamp when the limit resets
Position in middleware stack: After Retry, before ErrorHandler This ensures we capture headers from the final response (after retries) and can extract rate limit info even from error responses (429).
Constant Summary collapse
- LIMIT_HEADER =
Header name for total requests allowed per time window.
"x-ratelimit-limit"- REMAINING_HEADER =
Header name for requests remaining in current window.
"x-ratelimit-remaining"- RESET_HEADER =
Header name for Unix timestamp when the limit resets.
"x-ratelimit-reset"- DEFAULT_THRESHOLD =
Default throttle threshold (10% remaining). Rationale: 10% provides a safety buffer to avoid hitting 429 while not being overly conservative. At typical sec-api.io limits (~100 req/min), 10% = 10 requests buffer, which handles small bursts. Lower values risk 429s; higher values waste capacity. (Architecture ADR-4: Rate Limiting Strategy)
0.1- DEFAULT_QUEUE_WAIT_WARNING_THRESHOLD =
Default warning threshold for excessive wait times (5 minutes). Rationale: 5 minutes is long enough to indicate potential issues (API outage, misconfigured limits) but short enough to be actionable. Matches typical monitoring alert thresholds for request latency.
300- DEFAULT_QUEUE_WAIT_SECONDS =
Default wait time when rate limit is exhausted but reset_at is unknown (60 seconds). Rationale: sec-api.io rate limit windows are typically 60 seconds. When the API doesn’t send X-RateLimit-Reset header, this provides a reasonable fallback that aligns with expected window duration without excessive waiting.
60
Instance Method Summary collapse
-
#call(env) ⇒ Faraday::Response
Processes the request with rate limit queueing, throttling, and header extraction.
-
#initialize(app, options = {}) ⇒ RateLimiter
constructor
Creates a new RateLimiter middleware instance.
-
#queued_count ⇒ Integer
Returns the current count of queued (waiting) requests.
Constructor Details
#initialize(app, options = {}) ⇒ RateLimiter
Creates a new RateLimiter middleware instance.
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/sec_api/middleware/rate_limiter.rb', line 109 def initialize(app, = {}) super(app) @state_store = [:state_store] @threshold = .fetch(:threshold, DEFAULT_THRESHOLD) @on_throttle = [:on_throttle] @on_queue = [:on_queue] @on_dequeue = [:on_dequeue] @on_excessive_wait = [:on_excessive_wait] @queue_wait_warning_threshold = .fetch( :queue_wait_warning_threshold, DEFAULT_QUEUE_WAIT_WARNING_THRESHOLD ) @logger = [:logger] @log_level = .fetch(:log_level, :info) # Thread-safety design: Mutex + ConditionVariable pattern for efficient blocking. # Why not just sleep? Sleep wastes CPU cycles polling. ConditionVariable allows # threads to truly wait (zero CPU) until signaled, crucial for high-concurrency # workloads (Sidekiq, Puma) where many threads may be rate-limited simultaneously. # Why not atomic counters? We need to coordinate multiple operations (check state, # increment queue, wait) atomically, which requires a mutex. @mutex = Mutex.new @condition = ConditionVariable.new end |
Instance Method Details
#call(env) ⇒ Faraday::Response
Processes the request with rate limit queueing, throttling, and header extraction.
Before sending the request:
-
Generates a unique request_id (UUID) for tracing across callbacks
-
If rate limit is exhausted (remaining = 0), queues the request until reset
-
Otherwise, checks if below threshold and throttles if needed
After the response, extracts rate limit headers to update state.
154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/sec_api/middleware/rate_limiter.rb', line 154 def call(env) # Generate unique request_id for tracing across all callbacks request_id = env[:request_id] ||= SecureRandom.uuid wait_if_exhausted(request_id) throttle_if_needed(request_id) @app.call(env).on_complete do |response_env| extract_rate_limit_headers(response_env) signal_waiting_threads end end |
#queued_count ⇒ Integer
Returns the current count of queued (waiting) requests.
Delegates to the state store if available, otherwise returns 0.
138 139 140 |
# File 'lib/sec_api/middleware/rate_limiter.rb', line 138 def queued_count @state_store&.queued_count || 0 end |