we-promise/sure · error · Provider::Tiingo::RateLimitError
Tiingo unique symbol limit reached (#{MAX_SYMBOLS_PER_MONTH}
Error message
Tiingo unique symbol limit reached (#{MAX_SYMBOLS_PER_MONTH} per month) What it means
Tiingo's free tier allows 500 unique symbols per calendar month. track_symbol atomically claims a new symbol with a SETNX-style cache write ('tiingo:symbol:<YYYY-MM>:<SYMBOL>', 35-day TTL); only the claimer increments the month counter ('tiingo:symbol_count:<YYYY-MM>'). If the new count would reach MAX_SYMBOLS_PER_MONTH (500), the code rolls back (decrement counter, delete the symbol key) and raises RateLimitError -- so this symbol does not consume budget and remains unclaimed. Like the hourly gate, this is enforced client-side before the request.
Source
Thrown at app/models/provider/tiingo.rb:266
# Tracks unique symbols queried per month to stay within Tiingo's 500 symbols/month limit.
# Uses atomic set-if-absent (Redis SETNX) to eliminate the read-then-write race
# where two concurrent workers could both see the symbol as untracked and both
# increment the counter.
def track_symbol(symbol)
symbol_key = "tiingo:symbol:#{Date.current.strftime('%Y-%m')}:#{symbol.upcase}"
count_key = "tiingo:symbol_count:#{Date.current.strftime('%Y-%m')}"
# Atomic write-if-absent: returns false when the key already exists (Redis SETNX).
# Only the first worker to claim this symbol will proceed to increment the counter.
return unless Rails.cache.write(symbol_key, true, expires_in: 35.days, unless_exist: true)
new_count = Rails.cache.increment(count_key, 1, expires_in: 35.days).to_i
if new_count >= MAX_SYMBOLS_PER_MONTH
Rails.cache.decrement(count_key, 1)
Rails.cache.delete(symbol_key)
raise RateLimitError, "Tiingo unique symbol limit reached (#{MAX_SYMBOLS_PER_MONTH} per month)"
end
end
# min_request_interval provided by RateLimitable
def max_requests_per_hour
ENV.fetch("TIINGO_MAX_REQUESTS_PER_HOUR", MAX_REQUESTS_PER_HOUR).to_i
end
# Fetches the price currency for a symbol via the search endpoint.
# Only called as a fallback when the cache (populated by search_securities)
# doesn't have the currency. Raises on failure to avoid silently mislabeling
# non-USD instruments as USD.
def fetch_currency_for_symbol(symbol)
throttle_request
response = client.get("#{base_url}/tiingo/utilities/search") do |req|
req.params["query"] = symbolView on GitHub (pinned to e69894adb9)
Solutions
- Wait for the monthly rollover -- the keys are month-scoped (Date.current '%Y-%m') and expire after 35 days
- Upgrade the Tiingo plan or use a different provider for the extra symbols
- Trim what you fetch: only price securities actually held; check provider.usage (reads tiingo:symbol_count) before importing a batch
Example fix
# before
security.prices.each { |sym| provider.fetch_security_prices(symbol: sym, start_date: from, end_date: to) }
# after
return if provider.usage.utilization >= 100
security.prices.each { |sym| provider.fetch_security_prices(symbol: sym, start_date: from, end_date: to) } Defensive patterns
Strategy: validation
Validate before calling
# Read the monthly budget before introducing a new symbol
used = Rails.cache.read("tiingo:symbol_count:#{Date.current.strftime('%Y-%m')}").to_i
return if used >= 500 Try / catch
begin provider.fetch_security_prices(symbol:, start_date:, end_date:) rescue Provider::Tiingo::RateLimitError => e # rollback already ran; symbol is unclaimed, safe to retry next month or on another provider SecurityPriceFetch.set(wait: until_next_month).perform_later(...) end
Prevention
- Only fetch prices for securities actually held, not watchlists or search previews
- Surface provider.usage (used/500) in admin UI to see budget burn before it trips
When it happens
Trigger: The first fetch_security_prices call for a 501st distinct symbol in a calendar month (search-only symbols don't count; a symbol counts when prices are fetched). Repeats of already-tracked symbols never trigger it. Rollback means the same symbol can be retried next month or after an upgrade with no residue.
Common situations: Portfolio importer adding many tickers at once late in the month; users on self-hosted instances sharing one Tiingo key exhausting the shared budget; watchlist scans touching symbols never actually owned.
Related errors
- Tiingo hourly request limit reached (#{new_count}/#{max_requ
- rate_limited
- rate_limited
- Alpha Vantage daily request limit reached (#{max_requests_pe
- EODHD daily rate limit of #{max_requests_per_day} requests e
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/764444b33174391c.
Report an issue: GitHub.