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

429

429

Error message

Rate limit exceeded

What it means

Provider::TwelveData::RateLimitError raised by check_api_error! when the parsed response body carries code 429. Twelve Data signals rate limiting inside a 200-status JSON body ({code: 429, message: ...}), so this guard fires before the raise_error Faraday middleware ever sees an HTTP 429. The message is Twelve Data's own (e.g. 'You have exceeded your daily/monthly quota') or the default 'Rate limit exceeded'.

Source

Thrown at app/models/provider/twelve_data.rb:329

      # Set timestamp after all waits so the next call's 1s pacing is measured
      # from when this request actually fires, not from before the minute wait.
      @last_request_time = Time.current
    end

    def min_request_interval
      ENV.fetch("TWELVE_DATA_MIN_REQUEST_INTERVAL", MIN_REQUEST_INTERVAL).to_f
    end

    def max_requests_per_minute
      ENV.fetch("TWELVE_DATA_MAX_REQUESTS_PER_MINUTE", 7).to_i
    end

    def check_api_error!(parsed)
      return unless parsed.is_a?(Hash) && parsed["code"].present?

      if parsed["code"] == 429
        raise RateLimitError, parsed["message"] || "Rate limit exceeded"
      end

      raise Error, "API error (code: #{parsed["code"]}): #{parsed["message"] || "Unknown error"}"
    end

    def default_error_transformer(error)
      case error
      when RateLimitError
        error
      when Faraday::TooManyRequestsError
        RateLimitError.new("TwelveData rate limit exceeded", details: error.response&.dig(:body))
      when Faraday::Error
        self.class::Error.new(error.message, details: error.response&.dig(:body))
      else
        self.class::Error.new(error.message)
      end
    end
end

View on GitHub (pinned to e69894adb9)

Solutions

  1. Upgrade the Twelve Data plan or reduce request volume (cache results, batch lookups) if the daily quota is exhausted — waiting won't help until quota reset
  2. Align TWELVE_DATA_MAX_REQUESTS_PER_MINUTE with your plan's real per-minute limit so the built-in throttle works
  3. For per-minute bursts, back off ~60s and retry once; ensure all processes share the same Rails.cache so the credit counter is global
  4. Monitor provider.usage (daily_usage vs plan_daily_limit) before starting large syncs

Example fix

// before
rates = provider.fetch_exchange_rates(from: "EUR", to: "USD", start_date: s, end_date: e)

// after
begin
  rates = provider.fetch_exchange_rates(from: "EUR", to: "USD", start_date: s, end_date: e)
rescue Provider::TwelveData::RateLimitError => e
  raise if e.message.include?("daily") # daily quota: stop, don't retry
  sleep 60
  retry
end
Defensive patterns

Strategy: retry

Validate before calling

usage = provider.usage
if usage.utilization >= 95
  # defer non-critical syncs before hitting the daily quota
end

Try / catch

begin
  provider.fetch_exchange_rates(from:, to:, start_date:, end_date:)
rescue Provider::TwelveData::RateLimitError => e
  raise if e.message =~ /daily|monthly/ # quota exhausted: retrying now is pointless
  sleep 60
  retry
end

Prevention

When it happens

Trigger: Exceeding the per-minute credit limit (default 8/min on free plan; the client's throttle assumes 7), burning the daily quota (8 credits/day free tier), or the 5-credit time_series/cross call jumping past the remaining balance. Parallel processes each running their own throttle can combine to breach it.

Common situations: Free-plan daily quota exhausted mid-sync; multiple app instances/threads sharing one API key with independent throttle counters (the cache counter is only shared when Rails.cache is shared); the cross-listing fallback consuming 5 credits unexpectedly.

Related errors


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