{"record":{"id":"cd9f945c613a7a54","repo":"we-promise/sure","slug":"detail","errorCode":null,"errorMessage":"detail","messagePattern":"detail","errorType":"exception","errorClass":"Provider::Tiingo::RateLimitError","httpStatus":null,"severity":"warning","filePath":"app/models/provider/tiingo.rb","lineNumber":339,"sourceCode":"    # and a CA entry). The /tiingo/daily price endpoints this resolves\n    # currency for are US-centric (also confirmed live: a Canadian ticker\n    # like VFV has search metadata but no daily price history at all), so\n    # when a US entry exists for the ticker, that's the one actually backing\n    # the price data. Fall back to the first match otherwise.\n    def best_match_for_ticker(results, ticker)\n      return nil if ticker.blank?\n\n      matches = results.select { |s| s[\"ticker\"]&.upcase == ticker.upcase }\n      matches.find { |s| s[\"countryCode\"] == \"US\" } || matches.first\n    end\n\n    def check_api_error!(parsed)\n      return unless parsed.is_a?(Hash) && parsed[\"detail\"].present?\n\n      detail = parsed[\"detail\"]\n\n      if detail.downcase.include?(\"rate limit\") || detail.downcase.include?(\"too many\")\n        raise RateLimitError, detail\n      end\n\n      raise Error, \"API error: #{detail}\"\n    end\nend\n","sourceCodeStart":321,"sourceCodeEnd":345,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/tiingo.rb#L321-L345","documentation":"check_api_error! inspects every parsed Tiingo response; when the payload is a Hash whose 'detail' string mentions 'rate limit' or 'too many' (case-insensitive), it raises RateLimitError with that detail verbatim. Unlike the client-side gates (hourly counter, monthly symbol budget), this is the SERVER telling you the key/plan limit was hit -- the message you see is Tiingo's own text (e.g. 'You have exceeded the maximum number of requests per day' or 'Too many requests').","triggerScenarios":"Any Tiingo request whose response body is {'detail': '...rate limit...'} or '...too many...': daily request cap on the free key, burst throttling, or multiple app instances hammering the same key from outside this app's own counters (e.g. another deployment sharing the key).","commonSituations":"Self-hosted instance sharing a Tiingo API key with another app whose usage is invisible to this app's Redis counters; free-tier 50 requests/day-style limits on some endpoints; upstream tightening throttles while TIINGO_MAX_REQUESTS_PER_HOUR stays high.","solutions":["Wait and retry with backoff -- the detail usually states the window (per minute/per day)","Check the key's actual quota on tiingo.com and align client caps: lower TIINGO_MAX_REQUESTS_PER_HOUR so you stop before the server limit","Ensure no other deployment/tool shares this API key; if it does, split keys or add an external limiter"],"exampleFix":"# before\nbegin\n  provider.fetch_security_prices(symbol: sym, start_date: from, end_date: to)\nrescue Provider::Tiingo::RateLimitError\n  raise\nend\n\n# after\nbegin\n  provider.fetch_security_prices(symbol: sym, start_date: from, end_date: to)\nrescue Provider::Tiingo::RateLimitError => e\n  SyncRetryJob.set(wait: 15.minutes).perform_later(sym, from, to)\n  Rails.logger.warn(\"Tiingo rate limited: #{e.message}\")\nend","handlingStrategy":"retry","validationCode":null,"typeGuard":"def tiingo_rate_limited?(err)\n  err.is_a?(Provider::Tiingo::RateLimitError)\nend","tryCatchPattern":"begin\n  provider.fetch_security_prices(...)\nrescue Provider::Tiingo::RateLimitError => e\n  SyncRetryJob.set(wait: backoff_for(e.message)).perform_later(...) # server-side limit: wait it out\nend","preventionTips":["Keep the client-side hourly cap below the server-side plan limit so the local gate trips first","Ensure only one deployment consumes a given Tiingo key"],"tags":["tiingo","rate-limit","http-429","api-quota","retry"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}