{"record":{"id":"8d0aa331596363fe","repo":"we-promise/sure","slug":"rate-limited-8d0aa3","errorCode":"rate_limited","errorMessage":"Questrade rate limit exceeded. Please try again later.","messagePattern":"Questrade rate limit exceeded\\. Please try again later\\.","errorType":"exception","errorClass":"Provider::Questrade::RetryableResponseError","httpStatus":429,"severity":"warning","filePath":"app/models/provider/questrade.rb","lineNumber":260,"sourceCode":"        metadata: { status: response.code, body: response.body.to_s.first(1000) }\n      )\n    end\n\n    def handle_response(response)\n      case response.code\n      when 200, 201\n        JSON.parse(response.body, symbolize_names: true)\n      when 400\n        capture_response_error(\"bad_request\", response)\n        raise Error.new(\"Questrade bad request (#{response.code})\", :bad_request)\n      when 401\n        raise AuthenticationError.new(\"Invalid or expired Questrade credentials\", :unauthorized)\n      when 403\n        raise AuthenticationError.new(\"Access forbidden - check your permissions\", :access_forbidden)\n      when 404\n        raise Error.new(\"Resource not found\", :not_found)\n      when 429\n        raise RetryableResponseError.new(\"Questrade rate limit exceeded. Please try again later.\", :rate_limited)\n      when 500..599\n        raise RetryableResponseError.new(\"Questrade server error (#{response.code}). Please try again later.\", :server_error)\n      else\n        capture_response_error(\"unexpected_response\", response)\n        raise Error.new(\"Questrade unexpected response (#{response.code})\", :unknown)\n      end\n    end\nend\n","sourceCodeStart":242,"sourceCodeEnd":269,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/questrade.rb#L242-L269","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"# before\nids.each_slice(1) { |slice| provider.get_symbols(ids: slice) }\n\n# after\nbegin\n  provider.get_symbols(ids: ids) # single batched call\nrescue Provider::Questrade::RetryableResponseError => e\n  raise if e.error_type != :rate_limited\n  SyncJob.perform_in(5.minutes, item.id)\nend","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  provider.get_symbols(ids: ids)\nrescue Provider::Questrade::RetryableResponseError => e\n  raise if e.error_type != :rate_limited\n  SyncJob.perform_in(5.minutes, item.id)\nend","preventionTips":["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."],"tags":["questrade","http-429","rate-limit","retryable","brokerage-api"],"backgroundTag":"http-429-rate-limited","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}