instructure/canvas-lms · warning · RateLimited

Rate limit exceeded

Error message

Rate limit exceeded

What it means

StudyAssist::RateLimited is raised when the underlying Cedar LLM call throws InstLLMHelper::RateLimitExceededError, meaning the LLM provider rejected the request because a rate limit (per user/token/account) was exceeded. The service rescues the low-level error, logs a warning, and re-raises it as a domain-specific RateLimited error preserving the message.

Solutions

  1. Back off and retry the call after the rate-limit window (honor Retry-After if available)
  2. Inspect llm_config for the Cedar/InstLLM credentials and request a higher quota/tier
  3. Add client-side throttling/queueing in front of StudyAssist calls
  4. Cache or reuse recent LLM responses for identical prompts/content

Example fix

// before
result = StudyAssist.new(course:, user:, prompt:).call
// after
begin
  result = StudyAssist.new(course:, user:, prompt:).call
rescue StudyAssist::RateLimited => e
  sleep(backoff)
  retry
end
Defensive patterns

Strategy: retry

Validate before calling

null

Type guard

null

Try / catch

begin
  StudyAssist.new(course:, user:, prompt:, page_id:).call
rescue StudyAssist::RateLimited => e
  Rails.logger.warn("rate limited: #{e.message}")
  retry_after_backoff
end

Prevention

When it happens

Trigger: Calling StudyAssist#call with a valid prompt, enabled features, and resolvable content, but the Cedar/InstLLM backend returns 429 because the request quota was exhausted (too many requests in the window).

Common situations: Bursty student usage hitting shared LLM API quotas; missing or low rate-limit tier on the configured API key; batch jobs or retries fanning out calls; integration tests hammering the endpoint.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/57f30bb6e4997203. Report an issue: GitHub.

Appendix: source

Thrown at app/services/study_assist.rb:122

      end

      content = resolve_content

      llm_config = LLMConfigs.config_for(tool_config[:llm_config])
      raise "No LLM config found for #{tool_config[:llm_config]}" if llm_config.nil?

      cache_key = response_cache_key(tool_key, llm_config, content)
      Rails.cache.delete(cache_key) if @regenerate

      Rails.cache.fetch(cache_key, expires_in: RESPONSE_CACHE_TTL) do
        InstLLMHelper.with_rate_limit(user: @user, llm_config:) do
          raw = call_cedar(tool_key, llm_config, content)
          build_response(tool_key, raw)
        end
      end
    rescue InstLLMHelper::RateLimitExceededError => e
      Rails.logger.warn("Study Assist rate limit exceeded for #{tool_key}: #{e.message}")
      raise RateLimited, e.message
    end

    private

    def build_chips
      chips = TOOLS.each_with_object([]) do |(_, cfg), memo|
        memo << { chip: cfg[:chip_label], prompt: cfg[:chip_label] } if @course.feature_enabled?(cfg[:feature_flag])
      end
      { chips: }
    end

    # --- Content resolution ---

    def resolve_content
      page_id = @state["pageID"] || @state[:pageID]
      file_id = @state["fileID"] || @state[:fileID]

      content =

View on GitHub (pinned to 1c9f0bb801)