we-promise/sure · warning · Provider::Binance::RateLimitError

Rate limit exceeded

Error message

Rate limit exceeded

What it means

Raised by Provider::Binance#handle_response when Binance returns HTTP 429. Binance enforces IP-based request-weight limits (e.g. 6,000 weight/min per IP on spot); each endpoint costs weight, and when the bucket is exhausted every request gets 429. Sustained 429s escalate to HTTP 418 (IP auto-ban), so this error must be handled with backoff, not immediate retries.

Source

Thrown at app/models/provider/binance.rb:184

    def sign(params)
      query_string = params.is_a?(Hash) ? URI.encode_www_form(params.sort) : params
      OpenSSL::HMAC.hexdigest("sha256", api_secret, query_string)
    end

    def auth_headers
      { "X-MBX-APIKEY" => api_key }
    end

    def handle_response(response)
      parsed = response.parsed_response

      case response.code
      when 200..299
        parsed
      when 401
        raise AuthenticationError, extract_error_message(parsed) || "Unauthorized"
      when 429
        raise RateLimitError, "Rate limit exceeded"
      else
        msg = extract_error_message(parsed) || "API error: #{response.code}"
        raise InvalidSymbolError, msg if parsed.is_a?(Hash) && parsed["code"] == -1121
        raise ApiError, msg
      end
    end

    def extract_error_message(parsed)
      return parsed if parsed.is_a?(String)
      return nil unless parsed.is_a?(Hash)
      parsed["msg"] || parsed["message"] || parsed["error"]
    end
end

View on GitHub (pinned to e69894adb9)

Solutions

  1. Retry with exponential backoff honoring the Retry-After header (start >= 60s for weight-limit bans; 2s for transient bursts).
  2. Throttle request emission: include/throttle via Provider::RateLimitable or set a min interval between calls in the loop.
  3. Reduce weight: fetch wider kline intervals/ranges per request instead of many small calls, and batch symbols where the endpoint allows.
  4. If the IP is shared, lower per-process concurrency (Sidekiq concurrency, job queues) or move Binance calls to a single dedicated worker.
  5. If requests now fail with 418, the IP is temporarily banned — stop retrying and wait out the ban window (typically 2 min to 3 days) before resuming at a lower rate.

Example fix

# before
prices = provider.fetch_security_prices(symbol: s, exchange_operating_mic: mic, start_date: from, end_date: to)

# after
attempts = 0
begin
  attempts += 1
  prices = provider.fetch_security_prices(symbol: s, exchange_operating_mic: mic, start_date: from, end_date: to)
rescue Provider::Binance::RateLimitError
  raise if attempts >= 5
  sleep(2**attempts)
  retry
end
Defensive patterns

Strategy: retry

Type guard

def binance_rate_limited?(err)
  err.is_a?(Provider::Binance::RateLimitError)
end

Try / catch

attempts = 0
begin
  attempts += 1
  result = provider.fetch_security_prices(symbol: s, exchange_operating_mic: mic, start_date: from, end_date: to)
rescue Provider::Binance::RateLimitError
  raise if attempts >= 5
  sleep(2**attempts + rand(2))
  retry
end

Prevention

When it happens

Trigger: Calling fetch_security_prices or account endpoints in a tight loop (backfilling many securities) so weight accumulates past the per-minute cap; batch imports that page klines per symbol without a delay; multiple app instances or jobs sharing one egress IP; running alongside other Binance tooling on the same server.

Common situations: A nightly backfill job iterating hundreds of symbols; Sidekiq concurrency multiplying requests; deploying to a shared NAT gateway whose IP is also used by other Binance clients; forgetting that Binance counts weight per IP, not per API key; getting intermittent 429s that become permanent 418 because code retries without waiting.

Related errors


AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21). Data as JSON: /api/errors/f278c7280fb2f0e6. Report an issue: GitHub.