{"record":{"id":"065a9a5dd2541bb7","repo":"we-promise/sure","slug":"rate-limit-exceeded-065a9a","errorCode":null,"errorMessage":"Rate limit exceeded","messagePattern":"Rate limit exceeded","errorType":"exception","errorClass":"Provider::Coinbase::RateLimitError","httpStatus":429,"severity":"warning","filePath":"app/models/provider/coinbase.rb","lineNumber":203,"sourceCode":"\n    def auth_headers(method, path)\n      {\n        \"Authorization\" => \"Bearer #{generate_jwt(method, path)}\",\n        \"Content-Type\" => \"application/json\"\n      }\n    end\n\n    def handle_response(response)\n      parsed = response.parsed_response\n\n      case response.code\n      when 200..299\n        parsed.is_a?(Hash) ? parsed : { \"data\" => parsed }\n      when 401\n        error_msg = extract_error_message(parsed) || \"Unauthorized - check your API key and secret\"\n        raise AuthenticationError, error_msg\n      when 429\n        raise RateLimitError, \"Rate limit exceeded\"\n      else\n        error_msg = extract_error_message(parsed) || \"API error: #{response.code}\"\n        raise ApiError, error_msg\n      end\n    end\n\n    def extract_error_message(parsed)\n      return parsed if parsed.is_a?(String)\n      return nil unless parsed.is_a?(Hash)\n\n      parsed.dig(\"errors\", 0, \"message\") || parsed[\"error\"] || parsed[\"message\"]\n    end\nend\n","sourceCodeStart":185,"sourceCodeEnd":217,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/coinbase.rb#L185-L217","documentation":"Raised by Provider::Coinbase#handle_response when Coinbase returns HTTP 429. Coinbase rate-limits per API key and per endpoint family (private endpoints have point/second budgets). The client raises Provider::Coinbase::RateLimitError with the fixed message \"Rate limit exceeded\"; no Retry-After details are propagated.","triggerScenarios":"Polling account balances or prices in a tight loop; paginating many accounts/transactions without a delay; multiple jobs or app instances sharing the same API key; bursts after a deploy restarts several sync workers at once.","commonSituations":"Sync schedulers that fan out per-account Coinbase calls concurrently; retry storms where a timed-out call is immediately retried and compounds the limit; shared keys between production and side projects; upgrading plans/limits without adjusting poll frequency.","solutions":["Retry with exponential backoff (start ~1-2s) and jitter; treat 429 as transient, never fail the job on first hit.","Space out polling/pagination with a minimum interval between Coinbase calls.","Reduce concurrency: serialize Coinbase requests per API key (single worker or mutex).","Cache responses where possible (balances/price snapshots) to lower call volume.","If limits are chronically hit, request a rate-limit increase for the CDP key."],"exampleFix":"# before\naccounts = provider.get_accounts\n\n# after\nattempts = 0\nbegin\n  attempts += 1\n  accounts = provider.get_accounts\nrescue Provider::Coinbase::RateLimitError\n  raise if attempts >= 5\n  sleep((2**attempts) + rand)\n  retry\nend","handlingStrategy":"retry","validationCode":null,"typeGuard":"def coinbase_rate_limited?(err)\n  err.is_a?(Provider::Coinbase::RateLimitError)\nend","tryCatchPattern":"attempts = 0\nbegin\n  attempts += 1\n  accounts = provider.get_accounts\nrescue Provider::Coinbase::RateLimitError\n  raise if attempts >= 5\n  sleep((2**attempts) + rand)\n  retry\nend","preventionTips":["Serialize Coinbase calls per API key (one worker or mutex) to avoid concurrent bursts.","Space pagination calls with a minimum interval.","Cache balance/price snapshots to cut call volume.","Use jittered backoff; if 429s are constant, request a limit increase for the key."],"tags":["coinbase","rate-limit","http-429","backoff","crypto"],"backgroundTag":"rate-limit-429","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}