{"record":{"id":"9ac26e595ed0f071","repo":"we-promise/sure","slug":"rate-limited-9ac26e","errorCode":"rate_limited","errorMessage":"Rate limit exceeded. Please try again later.","messagePattern":"Rate limit exceeded\\. Please try again later\\.","errorType":"exception","errorClass":"Error","httpStatus":429,"severity":"warning","filePath":"app/models/provider/indexa_capital.rb","lineNumber":210,"sourceCode":"    def handle_response(response)\n      case response.code\n      when 200, 201\n        begin\n          JSON.parse(response.body, symbolize_names: true)\n        rescue JSON::ParserError => e\n          raise Error.new(\"Invalid JSON in response: #{e.message}\", :bad_response)\n        end\n      when 400\n        Rails.logger.error \"IndexaCapital API: Bad request - #{response.body}\"\n        raise Error.new(\"Bad request: #{response.body}\", :bad_request)\n      when 401\n        raise AuthenticationError.new(\"Invalid 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 Error.new(\"Rate limit exceeded. Please try again later.\", :rate_limited)\n      when 500..599\n        raise Error.new(\"IndexaCapital server error (#{response.code}). Please try again later.\", :server_error)\n      else\n        Rails.logger.error \"IndexaCapital API: Unexpected response - Code: #{response.code}, Body: #{response.body}\"\n        raise Error.new(\"Unexpected error: #{response.code} - #{response.body}\", :unknown)\n      end\n    end\n\n    # Extract accounts array from /users/me response\n    # API returns: { accounts: [{ account_number: \"ABC12345\", type: \"mutual\", status: \"active\", ... }] }\n    def extract_accounts(user_data)\n      accounts = user_data[:accounts] || []\n      accounts.map do |acct|\n        {\n          account_number: acct[:account_number],\n          name: account_display_name(acct),\n          type: acct[:type],\n          status: acct[:status],","sourceCodeStart":192,"sourceCodeEnd":228,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/indexa_capital.rb#L192-L228","documentation":"api.indexacapital.com answered HTTP 429 - Indexa throttles requests per token/IP. Raised as Error(:rate_limited) with no Retry-After surfaced, so the caller owns pacing and backoff. Unlike transport errors, 429 responses are not retried by with_retries (it only rescues socket-level exceptions).","triggerScenarios":"Looping get_account_balance/get_portfolio across many accounts in one tight loop; scheduled syncs piling up (every user at :00); the same API token shared by dev, staging and prod tripling the request rate.","commonSituations":"Fan-out sync jobs without throttling, retry storms after a 5xx, aggressive polling during market hours for performance data.","solutions":["Retry with exponential backoff (start ~30-60s) and jitter; Indexa's window resets on its own","Serialize Indexa calls per token and add a minimum interval between account fetches","Stagger sync schedules (spread over the hour) instead of a single cron slot","Give dev/staging separate tokens so they don't consume production's quota"],"exampleFix":"# before\naccounts.each { |a| provider.get_account_balance(account_number: a[:account_number]) }\n\n# after\naccounts.each_with_index do |a, i|\n  sleep 0.5 if i.positive?\n  provider.get_account_balance(account_number: a[:account_number])\nrescue Provider::IndexaCapital::Error => e\n  retry if (e.error_type == :rate_limited) && (sleep(30) || true) # simplified\nend","handlingStrategy":"retry","validationCode":null,"typeGuard":"def indexa_rate_limited?(error)\n  error.is_a?(Provider::IndexaCapital::Error) && error.error_type == :rate_limited\nend","tryCatchPattern":"retries = 0\nbegin\n  provider.get_account_balance(account_number: num)\nrescue Provider::IndexaCapital::Error => e\n  retries += 1\n  retry if e.error_type == :rate_limited && retries <= 4 && sleep((2**retries) + rand(4))\n  raise\nend","preventionTips":["Serialize Indexa calls per token with a minimum interval (e.g. 0.5s) between accounts","Stagger sync schedules instead of one cron slot for all users","Separate tokens per environment so dev doesn't burn prod quota","Remember with_retries will NOT retry 429 responses - only socket errors - so wrap calls yourself"],"tags":["indexa-capital","http-429","rate-limit","throttling","quota"],"backgroundTag":"http-429-rate-limited","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}