{"record":{"id":"57f30bb6e4997203","repo":"instructure/canvas-lms","slug":"rate-limit-exceeded-study-assist","errorCode":null,"errorMessage":"Rate limit exceeded","messagePattern":"Rate limit exceeded","errorType":"exception","errorClass":"RateLimited","httpStatus":null,"severity":"warning","filePath":"app/services/study_assist.rb","lineNumber":122,"sourceCode":"      end\n\n      content = resolve_content\n\n      llm_config = LLMConfigs.config_for(tool_config[:llm_config])\n      raise \"No LLM config found for #{tool_config[:llm_config]}\" if llm_config.nil?\n\n      cache_key = response_cache_key(tool_key, llm_config, content)\n      Rails.cache.delete(cache_key) if @regenerate\n\n      Rails.cache.fetch(cache_key, expires_in: RESPONSE_CACHE_TTL) do\n        InstLLMHelper.with_rate_limit(user: @user, llm_config:) do\n          raw = call_cedar(tool_key, llm_config, content)\n          build_response(tool_key, raw)\n        end\n      end\n    rescue InstLLMHelper::RateLimitExceededError => e\n      Rails.logger.warn(\"Study Assist rate limit exceeded for #{tool_key}: #{e.message}\")\n      raise RateLimited, e.message\n    end\n\n    private\n\n    def build_chips\n      chips = TOOLS.each_with_object([]) do |(_, cfg), memo|\n        memo << { chip: cfg[:chip_label], prompt: cfg[:chip_label] } if @course.feature_enabled?(cfg[:feature_flag])\n      end\n      { chips: }\n    end\n\n    # --- Content resolution ---\n\n    def resolve_content\n      page_id = @state[\"pageID\"] || @state[:pageID]\n      file_id = @state[\"fileID\"] || @state[:fileID]\n\n      content =","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/services/study_assist.rb#L104-L140","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Back off and retry the call after the rate-limit window (honor Retry-After if available)","Inspect llm_config for the Cedar/InstLLM credentials and request a higher quota/tier","Add client-side throttling/queueing in front of StudyAssist calls","Cache or reuse recent LLM responses for identical prompts/content"],"exampleFix":"// before\nresult = StudyAssist.new(course:, user:, prompt:).call\n// after\nbegin\n  result = StudyAssist.new(course:, user:, prompt:).call\nrescue StudyAssist::RateLimited => e\n  sleep(backoff)\n  retry\nend","handlingStrategy":"retry","validationCode":"null","typeGuard":"null","tryCatchPattern":"begin\n  StudyAssist.new(course:, user:, prompt:, page_id:).call\nrescue StudyAssist::RateLimited => e\n  Rails.logger.warn(\"rate limited: #{e.message}\")\n  retry_after_backoff\nend","preventionTips":["Apply exponential backoff with jitter on retries","Throttle calls client-side per user","Monitor rate-limit warnings in logs","Use dedicated higher-quota LLM credentials for production"],"tags":["ruby","study-assist","rate-limit","llm","cedar"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}