{"record":{"id":"86ac5bb9d01b61bc","repo":"instructure/canvas-lms","slug":"you-ve-hit-the-study-tools-rate-limit-try-again-later","errorCode":null,"errorMessage":"You've hit the study tools rate limit. Try again later.","messagePattern":"You've hit the study tools rate limit\\. Try again later\\.","errorType":"http","errorClass":null,"httpStatus":429,"severity":"warning","filePath":"app/controllers/study_assist_controller.rb","lineNumber":75,"sourceCode":"    record_metric(\"unknown\", \"unsupported\")\n    render json: { error: t(\"Study tools aren't available for this prompt.\") }, status: :unprocessable_content\n  rescue StudyAssist::ToolDisabled\n    record_metric(tool_tag(request_body[:prompt]), \"unsupported\")\n    render json: { error: t(\"This study tool isn't currently available.\") }, status: :forbidden\n  rescue StudyAssist::UnsupportedContentType\n    record_metric(tool_tag(request_body[:prompt]), \"unsupported\")\n    render json: { error: t(\"Study tools aren't available for this file type.\") }, status: :unprocessable_content\n  rescue StudyAssist::ContentUnavailable => e\n    record_metric(tool_tag(request_body[:prompt]), \"content_unavailable\")\n    Rails.logger.info(\"Study Assist content unavailable: #{e.message}\")\n    render json: { error: t(\"Content isn't available for study tools.\") }, status: :unprocessable_content\n  rescue StudyAssist::ContentTooLarge\n    record_metric(tool_tag(request_body[:prompt]), \"too_large\")\n    render json: { error: t(\"Content is too long for study tools.\") }, status: :unprocessable_content\n  rescue StudyAssist::RateLimited => e\n    record_metric(tool_tag(request_body[:prompt]), \"rate_limit\")\n    Rails.logger.warn(\"Study Assist rate limit: #{e.message}\")\n    render json: { error: t(\"You've hit the study tools rate limit. Try again later.\") }, status: :too_many_requests\n  rescue StudyAssist::CedarUnavailable => e\n    record_metric(tool_tag(request_body[:prompt]), \"cedar_error\")\n    Rails.logger.error(\"Study Assist Cedar failure: #{e.message}\")\n    render json: { error: t(\"Study tools are temporarily unavailable. Please try again.\") }, status: :bad_gateway\n  end\n\n  private\n\n  def require_study_assist_enabled\n    return if @context.is_a?(Course) && @context.feature_enabled?(:study_assist)\n\n    render json: { error: t(\"Study tools aren't enabled for this course.\") }, status: :not_found\n    false\n  end\n\n  def require_student_access\n    authorized_action(@context, @current_user, :participate_as_student)\n  end","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/controllers/study_assist_controller.rb#L57-L93","documentation":"StudyAssistController#create rescues StudyAssist::RateLimited raised by the upstream study-tools/Cedar pipeline and renders a 429 with this message. The client exceeded the configured rate limit for AI study tools prompts within the allowed window.","triggerScenarios":"A user or integration POSTs prompts to the study assist create endpoint more times than the per-user/account limit allows in the rate window, causing the service layer to raise StudyAssist::RateLimited.","commonSituations":"Loops or batch scripts hammering the study assist endpoint; shared accounts consumed by multiple users; classroom-wide usage spikes; aggressively-retrying clients after failures.","solutions":["Wait for the rate-limit window to elapse and retry with backoff","Reduce request frequency / batch prompts in the client","Check the rate limit configuration (per user/account) and raise it if legitimately too low","Record which request triggered it via the logged 'Study Assist rate limit' warning and throttle client-side"],"exampleFix":"// before\n// client retries immediately on failure\nfor (const p of prompts) await send(p)\n// after\nfor (const p of prompts) {\n  const res = await send(p)\n  if (res.status === 429) await sleep(Math.pow(2, attempt++) * 1000)\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"const res = await fetch(url, opts)\nif (res.status === 429) {\n  await sleep(Math.min(backoffMs *= 2, 60_000))\n  return retry()\n}","preventionTips":["Throttle client request rates within documented limits","Use exponential backoff with jitter on 429s","Batch prompts instead of sending one per request","Monitor per-user usage against the limit"],"tags":["rate-limit","http-429","ai-tools"],"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"}