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

EODHD daily rate limit of #{max_requests_per_day} requests e

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.

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)

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.