we-promise/sure · warning · Provider::Questrade::RetryableResponseError
rate_limited
rate_limited
Error message
Questrade rate limit exceeded. Please try again later.
What it means
Raised by Provider::Questrade#handle_response on HTTP 429, wrapped in RetryableResponseError — a distinct class signaling 'safe to retry later'. Questrade throttles per-token request rates and per-API quotas (e.g. heavy symbol or activity polling), and marks the class of error as retryable rather than fatal like the Error subclasses.
Source
Thrown at app/models/provider/questrade.rb:260
metadata: { status: response.code, body: response.body.to_s.first(1000) }
)
end
def handle_response(response)
case response.code
when 200, 201
JSON.parse(response.body, symbolize_names: true)
when 400
capture_response_error("bad_request", response)
raise Error.new("Questrade bad request (#{response.code})", :bad_request)
when 401
raise AuthenticationError.new("Invalid or expired Questrade credentials", :unauthorized)
when 403
raise AuthenticationError.new("Access forbidden - check your permissions", :access_forbidden)
when 404
raise Error.new("Resource not found", :not_found)
when 429
raise RetryableResponseError.new("Questrade rate limit exceeded. Please try again later.", :rate_limited)
when 500..599
raise RetryableResponseError.new("Questrade server error (#{response.code}). Please try again later.", :server_error)
else
capture_response_error("unexpected_response", response)
raise Error.new("Questrade unexpected response (#{response.code})", :unknown)
end
end
end
View on GitHub (pinned to e69894adb9)
Solutions
- Catch RetryableResponseError specifically and reschedule the sync with backoff (minutes, not seconds).
- Batch symbol lookups through get_symbols(ids:) which joins and deduplicates ids into one request.
- Space per-account calls in multi-account sync loops and serialize syncs per token.
- Track Questrade's documented rate tiers and stay under them (upgrade tier if the workload legitimately needs more).
Example fix
# before
ids.each_slice(1) { |slice| provider.get_symbols(ids: slice) }
# after
begin
provider.get_symbols(ids: ids) # single batched call
rescue Provider::Questrade::RetryableResponseError => e
raise if e.error_type != :rate_limited
SyncJob.perform_in(5.minutes, item.id)
end Defensive patterns
Strategy: retry
Try / catch
begin provider.get_symbols(ids: ids) rescue Provider::Questrade::RetryableResponseError => e raise if e.error_type != :rate_limited SyncJob.perform_in(5.minutes, item.id) end
Prevention
- Batch symbol IDs in one get_symbols call (it dedupes for you).
- Serialize per-token syncs and pace multi-account loops.
- Rescue RetryableResponseError distinctly from fatal errors so throttles become delays, not failures.
When it happens
Trigger: Polling get_symbols for many positions in tight succession; syncing many accounts' activities in one loop without pacing; multiple concurrent syncs on the same token; re-running a failed backfill that duplicates the request volume.
Common situations: Interval jobs overlapping (every-minute cron plus a backlog worker); symbol enrichment calling get_symbols per-position instead of batching the ids (the method already batches — bypassing it hurts); test suites hammering the live API.
Related errors
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/8d0aa331596363fe.
Report an issue: GitHub.