we-promise/sure · error · Provider::Eodhd::RateLimitError

EODHD daily rate limit of #

Error message

EODHD daily rate limit of #{max_requests_per_day} requests exhausted

What it means

Raised as Provider::Eodhd::RateLimitError when the shared daily counter in Rails.cache ('eodhd:daily:<Date.current>') exceeds max_requests_per_day (default from MAX_REQUESTS_PER_DAY, overridable via EODHD_MAX_REQUESTS_PER_DAY). The counter is incremented atomically before each request (increment-then-check), so concurrent workers cannot undercount. Once the cap is hit, every further EODHD call that day raises immediately without touching the network.

Solutions

  1. Wait for the UTC day to roll over — the cache key is date-scoped ('eodhd:daily:<date>') and resets naturally
  2. Raise the budget if your plan allows: set EODHD_MAX_REQUESTS_PER_DAY in the environment to your plan's real quota
  3. Check for runaway callers: DebugLogEntry / logs showing unexpected EODHD request volume (each failed retry still increments)
  4. Ensure Rails.cache is a shared store (Redis/Memcached) in multi-process deployments so all workers see one counter
  5. Batch or schedule price backfills across days, and cache per-symbol/day results so repeated valuations do not re-query EODHD

Example fix

# before: every valuation day re-fetches all prices, exhausting the daily cap
holdings.each { |h| provider.fetch_security_price(symbol: h.symbol, date: Date.current) }

# after: cache results per symbol/date so the daily budget is spent once
holdings.each do |h|
  key = "price:#{h.symbol}:#{Date.current}"
  price = Rails.cache.fetch(key, expires_in: 12.hours) do
    provider.fetch_security_price(symbol: h.symbol, date: Date.current)
  end
end
Defensive patterns

Strategy: retry

Validate before calling

# Check remaining budget before starting an expensive backfill
REMAINING = provider.max_requests_per_day - Rails.cache.read("eodhd:daily:#{Date.current}").to_i
BUDGET_NEEDED = symbols.size # one request per symbol window

if REMAINING < BUDGET_NEEDED
  ScheduleBackfillJob.set(wait_until: Date.tomorrow.midnight + 5.minutes).perform_later(...)
  return
end

Try / catch

begin
  provider.fetch_security_prices(...)
rescue Provider::Eodhd::RateLimitError
  # daily cap is calendar-scoped: reschedule for after UTC midnight, do not retry now
  SyncJob.set(wait_until: Date.tomorrow.midnight + 5.minutes).perform_later(job.id)
end

Prevention

When it happens

Trigger: Any EODHD API call (search, fetch_security_prices, fetch_security_price) after the day's counter has already reached the cap — e.g. a large multi-security backfill issuing thousands of EOD requests, multiple Sidekiq workers syncing in parallel, or repeated failed runs each burning increments. The counter is cache-global, not per-account, so all EODHD usage shares the budget.

Common situations: Initial import of a portfolio with many securities, retries of a failing sync each consuming requests, a misconfigured cache store (e.g. memory_store per-process) that under- or over-counts, EODHD_MAX_REQUESTS_PER_DAY left at the free-tier default while the app runs paid-tier volume, and month-end valuation jobs that price hundreds of holdings on one day.

Related errors


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

Appendix: source

Thrown at app/models/provider/eodhd.rb:291

      elsif exchange_operating_mic.present?
        "#{symbol}.#{exchange_operating_mic}"
      else
        "#{symbol}.US"
      end
    end

    # Cache key for tracking daily API usage
    def daily_cache_key
      "eodhd:daily:#{Date.current}"
    end

    # Enforces the daily rate limit. Raises RateLimitError if the limit is exhausted.
    # Uses atomic increment-then-check to avoid TOCTOU races between concurrent workers.
    def enforce_daily_limit!
      new_count = Rails.cache.increment(daily_cache_key, 1, expires_in: 24.hours).to_i

      if new_count > max_requests_per_day
        raise RateLimitError, "EODHD daily rate limit of #{max_requests_per_day} requests exhausted"
      end
    end

    # throttle_request and min_request_interval provided by RateLimitable

    def max_requests_per_day
      ENV.fetch("EODHD_MAX_REQUESTS_PER_DAY", MAX_REQUESTS_PER_DAY).to_i
    end

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

      raise Error, "API error: #{parsed["error"]}"
    end
end

View on GitHub (pinned to e69894adb9)