{"record":{"id":"d132c962a346b65e","repo":"we-promise/sure","slug":"429","errorCode":"429","errorMessage":"Rate limit exceeded","messagePattern":"Rate limit exceeded","errorType":"exception","errorClass":"Provider::TwelveData::RateLimitError","httpStatus":429,"severity":"warning","filePath":"app/models/provider/twelve_data.rb","lineNumber":329,"sourceCode":"\n      # Set timestamp after all waits so the next call's 1s pacing is measured\n      # from when this request actually fires, not from before the minute wait.\n      @last_request_time = Time.current\n    end\n\n    def min_request_interval\n      ENV.fetch(\"TWELVE_DATA_MIN_REQUEST_INTERVAL\", MIN_REQUEST_INTERVAL).to_f\n    end\n\n    def max_requests_per_minute\n      ENV.fetch(\"TWELVE_DATA_MAX_REQUESTS_PER_MINUTE\", 7).to_i\n    end\n\n    def check_api_error!(parsed)\n      return unless parsed.is_a?(Hash) && parsed[\"code\"].present?\n\n      if parsed[\"code\"] == 429\n        raise RateLimitError, parsed[\"message\"] || \"Rate limit exceeded\"\n      end\n\n      raise Error, \"API error (code: #{parsed[\"code\"]}): #{parsed[\"message\"] || \"Unknown error\"}\"\n    end\n\n    def default_error_transformer(error)\n      case error\n      when RateLimitError\n        error\n      when Faraday::TooManyRequestsError\n        RateLimitError.new(\"TwelveData rate limit exceeded\", details: error.response&.dig(:body))\n      when Faraday::Error\n        self.class::Error.new(error.message, details: error.response&.dig(:body))\n      else\n        self.class::Error.new(error.message)\n      end\n    end\nend","sourceCodeStart":311,"sourceCodeEnd":347,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/twelve_data.rb#L311-L347","documentation":"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'.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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","Align TWELVE_DATA_MAX_REQUESTS_PER_MINUTE with your plan's real per-minute limit so the built-in throttle works","For per-minute bursts, back off ~60s and retry once; ensure all processes share the same Rails.cache so the credit counter is global","Monitor provider.usage (daily_usage vs plan_daily_limit) before starting large syncs"],"exampleFix":"// before\nrates = provider.fetch_exchange_rates(from: \"EUR\", to: \"USD\", start_date: s, end_date: e)\n\n// after\nbegin\n  rates = provider.fetch_exchange_rates(from: \"EUR\", to: \"USD\", start_date: s, end_date: e)\nrescue Provider::TwelveData::RateLimitError => e\n  raise if e.message.include?(\"daily\") # daily quota: stop, don't retry\n  sleep 60\n  retry\nend","handlingStrategy":"retry","validationCode":"usage = provider.usage\nif usage.utilization >= 95\n  # defer non-critical syncs before hitting the daily quota\nend","typeGuard":null,"tryCatchPattern":"begin\n  provider.fetch_exchange_rates(from:, to:, start_date:, end_date:)\nrescue Provider::TwelveData::RateLimitError => e\n  raise if e.message =~ /daily|monthly/ # quota exhausted: retrying now is pointless\n  sleep 60\n  retry\nend","preventionTips":["Set TWELVE_DATA_MAX_REQUESTS_PER_MINUTE to the plan's real limit so the built-in throttle protects you","Share Rails.cache across processes so the per-minute credit counter is global","Check provider.usage before large syncs and cache aggressively — the cross-listing fallback costs 5 credits per call"],"tags":["ruby","twelvedata","rate-limit","http-429-in-body","quota"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}