we-promise/sure · warning · Error

rate_limited

rate_limited

Error message

Rate limit exceeded. Please try again later.

What it means

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).

Source

Thrown at app/models/provider/indexa_capital.rb:210

    def handle_response(response)
      case response.code
      when 200, 201
        begin
          JSON.parse(response.body, symbolize_names: true)
        rescue JSON::ParserError => e
          raise Error.new("Invalid JSON in response: #{e.message}", :bad_response)
        end
      when 400
        Rails.logger.error "IndexaCapital API: Bad request - #{response.body}"
        raise Error.new("Bad request: #{response.body}", :bad_request)
      when 401
        raise AuthenticationError.new("Invalid credentials", :unauthorized)
      when 403
        raise AuthenticationError.new("Access forbidden - check your permissions", :access_forbidden)
      when 404
        raise Error.new("Resource not found", :not_found)
      when 429
        raise Error.new("Rate limit exceeded. Please try again later.", :rate_limited)
      when 500..599
        raise Error.new("IndexaCapital server error (#{response.code}). Please try again later.", :server_error)
      else
        Rails.logger.error "IndexaCapital API: Unexpected response - Code: #{response.code}, Body: #{response.body}"
        raise Error.new("Unexpected error: #{response.code} - #{response.body}", :unknown)
      end
    end

    # Extract accounts array from /users/me response
    # API returns: { accounts: [{ account_number: "ABC12345", type: "mutual", status: "active", ... }] }
    def extract_accounts(user_data)
      accounts = user_data[:accounts] || []
      accounts.map do |acct|
        {
          account_number: acct[:account_number],
          name: account_display_name(acct),
          type: acct[:type],
          status: acct[:status],

View on GitHub (pinned to e69894adb9)

Solutions

  1. Retry with exponential backoff (start ~30-60s) and jitter; Indexa's window resets on its own
  2. Serialize Indexa calls per token and add a minimum interval between account fetches
  3. Stagger sync schedules (spread over the hour) instead of a single cron slot
  4. Give dev/staging separate tokens so they don't consume production's quota

Example fix

# before
accounts.each { |a| provider.get_account_balance(account_number: a[:account_number]) }

# after
accounts.each_with_index do |a, i|
  sleep 0.5 if i.positive?
  provider.get_account_balance(account_number: a[:account_number])
rescue Provider::IndexaCapital::Error => e
  retry if (e.error_type == :rate_limited) && (sleep(30) || true) # simplified
end
Defensive patterns

Strategy: retry

Type guard

def indexa_rate_limited?(error)
  error.is_a?(Provider::IndexaCapital::Error) && error.error_type == :rate_limited
end

Try / catch

retries = 0
begin
  provider.get_account_balance(account_number: num)
rescue Provider::IndexaCapital::Error => e
  retries += 1
  retry if e.error_type == :rate_limited && retries <= 4 && sleep((2**retries) + rand(4))
  raise
end

Prevention

When it happens

Trigger: 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.

Common situations: Fan-out sync jobs without throttling, retry storms after a 5xx, aggressive polling during market hours for performance data.

Related errors


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