instructure/canvas-lms · error · Pv4TooManyRequests

rate limit exceeded

Error message

rate limit exceeded

What it means

Pv4Client#fetch raises Pv4TooManyRequests when the PV4 page-view service returns HTTP 429, meaning the client exceeded the service's rate limit. The library surfaces this as a typed error instead of retrying, letting callers decide on backoff.

Solutions

  1. Implement exponential backoff and retry the request after the delay indicated by the 429 response
  2. Cache page-view history results to reduce repeat calls to PV4
  3. Throttle/queue calls to PageView.for_user in background jobs
  4. Ask the PV4 service operators to raise the rate limit if legitimate traffic is being throttled

Example fix

// before
views = PageView.for_user(user)
// after
begin
  views = PageView.for_user(user)
rescue Pv4TooManyRequests
  sleep(backoff)
  retry
end
Defensive patterns

Strategy: retry

Try / catch

retries = 0
begin
  views = PageView.for_user(user)
rescue Pv4TooManyRequests
  retries += 1
  raise if retries > 3
  sleep(2**retries)
  retry
end

Prevention

When it happens

Trigger: Calling Pv4Client#fetch (via PageView.for_user) too frequently — e.g. polling page-view history in a tight loop or a burst of user requests hitting the same PV4 service.

Common situations: Automated scripts or monitoring repeatedly loading page-view history; many concurrent users requesting history during peak load; missing caching of history results on the Canvas side.

Related errors


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

Appendix: source

Thrown at app/models/page_view/pv4_client.rb:64

      params = "start_time=#{start_time.utc.iso8601(PRECISION)}"
      params << "&end_time=#{end_time.utc.iso8601(PRECISION)}"
      params << "&#{cached_root_account_uuids_for(user:)}"
      params << "&last_page_view_id=#{last_page_view_id}" if last_page_view_id
      params << "&limit=#{limit}" if limit
      response = CanvasHttp.get(
        @uri.merge("users/#{user.global_id}/page_views?#{params}").to_s,
        request_headers
      )

      case response.code.to_i
      when 400
        raise Pv4BadRequest, "invalid request"
      when 401
        raise Pv4Unauthorized, "unauthorized request"
      when 404
        raise Pv4NotFound, "resource not found"
      when 429
        raise Pv4TooManyRequests, "rate limit exceeded"
      end

      json =
        begin
          response.body.empty? ? {} : JSON.parse(response.body)
        rescue JSON::ParserError
          {}
        end
      raise Pv4EmptyResponse, "the response is empty or does not contain expected keys" unless json["page_views"]

      json["page_views"].map! do |pv|
        pv["session_id"] = pv.delete("sessionid")
        vhost = pv.delete("vhost")
        http_request = pv.delete("http_request")
        pv["url"] = if vhost.present? && http_request.present?
                      "#{HostUrl.protocol}://#{vhost}#{http_request}"
                    elsif http_request.present?
                      "#{HostUrl.protocol}:#{http_request}"

View on GitHub (pinned to 1c9f0bb801)