{"record":{"id":"629d3f655e47b1ff","repo":"we-promise/sure","slug":"rate-limited-629d3f","errorCode":"rate_limited","errorMessage":"Rate limit exceeded. Please try again later.","messagePattern":"Rate limit exceeded\\. Please try again later\\.","errorType":"exception","errorClass":"Provider::Mercury::MercuryError","httpStatus":429,"severity":"warning","filePath":"app/models/provider/mercury.rb","lineNumber":126,"sourceCode":"    end\n\n    def handle_response(response)\n      case response.code\n      when 200\n        JSON.parse(response.body, symbolize_names: true)\n      when 400\n        Rails.logger.error \"Mercury API: Bad request - #{response.body}\"\n        raise MercuryError.new(\"Bad request to Mercury API: #{response.body}\", :bad_request)\n      when 401\n        # Parse the error response for more specific messages\n        error_message = parse_error_message(response.body)\n        raise MercuryError.new(error_message, :unauthorized)\n      when 403\n        raise MercuryError.new(\"Access forbidden - check your API token permissions\", :access_forbidden)\n      when 404\n        raise MercuryError.new(\"Resource not found\", :not_found)\n      when 429\n        raise MercuryError.new(\"Rate limit exceeded. Please try again later.\", :rate_limited)\n      else\n        Rails.logger.error \"Mercury API: Unexpected response - Code: #{response.code}, Body: #{response.body}\"\n        raise MercuryError.new(\"Failed to fetch data: #{response.code} #{response.message} - #{response.body}\", :fetch_failed)\n      end\n    end\n\n    def parse_error_message(body)\n      parsed = JSON.parse(body, symbolize_names: true)\n      errors = parsed[:errors] || {}\n\n      case errors[:errorCode]\n      when \"ipNotWhitelisted\"\n        ip = errors[:ip] || \"unknown\"\n        \"IP address not whitelisted (#{ip}). Add your IP to the API token's whitelist in Mercury dashboard.\"\n      when \"noTokenInDBButMaybeMalformed\"\n        \"Invalid token format. Make sure to include the 'secret-token:' prefix.\"\n      else\n        errors[:message] || \"Invalid API token\"","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/mercury.rb#L108-L144","documentation":"Raised by Provider::Mercury#handle_response when Mercury returns HTTP 429 — you exceeded Mercury's API rate limit. Mercury enforces per-token request quotas (documented on the order of hundreds of requests per minute), and bursts such as paginating transactions across many accounts can trip it.","triggerScenarios":"Tight loops calling get_account_transactions with small limit/offset pages for one account; syncing many accounts concurrently with no throttle; a background job plus a manual sync running at the same time with the same token.","commonSituations":"Backfill jobs paginating transactions page-by-page with limit=10; parallel Sidekiq workers each hitting Mercury; retry storms after a transient 5xx that re-issue requests immediately; adding a new household with many Mercury accounts and syncing them all at once.","solutions":["Wait and retry with backoff — Mercury does not surface a Retry-After through this client, so start with tens of seconds and scale (e.g. 30s, 60s, 120s).","Page transactions with the largest limit Mercury allows instead of many small pages, reducing request count.","Serialize per-token syncs (a per-token lock or single worker lane) so concurrent jobs cannot burst.","Add spacing between accounts in a multi-account sync loop (sleep or a rate limiter like the MIN_REQUEST_INTERVAL pattern used by other providers here)."],"exampleFix":"# before\nloop do\n  page = provider.get_account_transactions(id, offset: off, limit: 10)\n  break if page[:transactions].empty?\n  off += 10\nend\n\n# after\nretry_on_rate_limit = ->(attempt) { sleep(30 * (2**attempt)) }\nattempt = 0\nbegin\n  loop do\n    page = provider.get_account_transactions(id, offset: off, limit: 100)\n    break if page[:transactions].empty?\n    off += 100\n  end\nrescue Provider::Mercury::MercuryError => e\n  raise if e.error_type != :rate_limited || (attempt += 1) > 3\n  retry_on_rate_limit.call(attempt)\n  retry\nend","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"attempts = 0\nbegin\n  provider.get_accounts\nrescue Provider::Mercury::MercuryError => e\n  raise if e.error_type != :rate_limited || (attempts += 1) >= 4\n  sleep(30 * (2 ** (attempts - 1)))\n  retry\nend","preventionTips":["Page with the largest limit Mercury allows rather than many small requests.","Serialize syncs that share a token and leave a small gap between per-account calls.","Budget the expected request count per sync and stay under Mercury's per-minute quota."],"tags":["mercury","http-429","rate-limit","backoff","banking-api"],"backgroundTag":"http-429-rate-limited","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}