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"] = symbol

View on GitHub (pinned to e69894adb9)

Solutions

  1. Wait for the monthly rollover -- the keys are month-scoped (Date.current '%Y-%m') and expire after 35 days
  2. Upgrade the Tiingo plan or use a different provider for the extra symbols
  3. 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

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


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