instructure/canvas-lms · error · ArgumentError

Cannot call with more than 1000 enrollments

Error message

Cannot call with more than 1000 enrollments

What it means

Enrollment::BatchStateUpdater.destroy_batch mass-deletes enrollments in one transaction, preloading users, observers and root accounts to fix up counters. The work is capped at 1000 enrollments to keep transactions and job payloads bounded; larger batches raise ArgumentError before any deletion.

Solutions

  1. Partition the batch into chunks of <= 1000 and call destroy_batch per chunk.
  2. Ensure batch is a proper relation/id scope so count is cheap.
  3. For very large deletes, use a dedicated workflow (e.g. course conclude/delete) instead of one batch call.

Example fix

// before
Enrollment::BatchStateUpdater.destroy_batch(enrollments)
// after
enrollments.find_in_batches(batch_size: 1000) { |chunk| Enrollment::BatchStateUpdater.destroy_batch(chunk) }
Defensive patterns

Strategy: validation

Validate before calling

enrollments.find_in_batches(batch_size: 1000) { |chunk| Enrollment::BatchStateUpdater.destroy_batch(chunk) }

Try / catch

begin
  Enrollment::BatchStateUpdater.destroy_batch(batch)
rescue ArgumentError
  batch.find_in_batches(batch_size: 1000) { |c| Enrollment::BatchStateUpdater.destroy_batch(c) }
end

Prevention

When it happens

Trigger: Calling destroy_batch(batch) where batch.count > 1000, e.g. deleting all enrollments of a large course or a big SIS batch-mode delete.

Common situations: Course merges/unpublishes, SIS imports deleting large enrollment sets, cleanup rake tasks passing unscoped relations.

Related errors


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

Appendix: source

Thrown at app/models/enrollment/batch_state_updater.rb:49

  # after_save :copy_scores_from_existing_enrollment, if: :need_to_copy_scores? - not needed for destroy
  # after_save :restore_submissions_and_scores - not needed for destroy
  # after_save :clear_email_caches - handled in clear_email_caches
  # after_save :cancel_future_appointments - handled in cancel_future_appointments
  # after_save :update_linked_enrollments - handled in update_linked_enrollments
  # after_save :set_update_cached_due_dates - handled in update_cached_due_dates
  # after_save :touch_graders_if_needed - handled in touch_all_graders_if_needed
  # after_save :reset_notifications_cache - handled in reset_notifications_cache
  # after_save :dispatch_invitations_later - not needed for destroy
  # after_save :add_to_favorites_later - not needed for destroy
  # after_commit :update_cached_due_dates - handled in update_cached_due_dates
  # after_save :update_assignment_overrides_if_needed - handled in update_assignment_overrides
  # after_save :needs_grading_count_updated -handled in needs_grading_count_updated
  # after_commit :sync_microsoft_group - handled in sync_microsoft_group
  #
  # ignore_due_date_caching_for: optional hash of (course_id => user_ids_array) of users to _not_
  # re-cache their scores for (presumably the caller already queued one of their own)
  def self.destroy_batch(batch, sis_batch: nil, batch_mode: false, ignore_due_date_caching_for: {})
    raise ArgumentError, "Cannot call with more than 1000 enrollments" if batch.count > 1000

    Enrollment.transaction do
      # cache some data before the destroy that is needed after the destroy
      @invited_user_ids = Enrollment.where(id: batch, workflow_state: "invited").distinct.pluck(:user_id)
      @students = Enrollment.of_student_type.where(id: batch).preload({ user: :linked_observers }, :root_account).to_a
      @students.each do |e|
        e.workflow_state = "deleted"
        e.readonly!
      end
      @user_course_tuples = Enrollment.where(id: batch).active.select(%i[user_id course_id]).distinct.to_a
      @user_ids = Enrollment.where(id: batch).order(:user_id).distinct.pluck(:user_id)
      @courses = Course.where(id: Enrollment.where(id: batch).select(:course_id).distinct).to_a
      @root_account = @courses.first.root_account
      @data = mark_enrollments_as_deleted(batch, sis_batch:, batch_mode:)
      gms = remove_group_memberships(batch, @courses, @user_ids, sis_batch:, batch_mode:)
      @data&.push(*gms) if gms
      # touch users after removing group_memberships to invalidate the cache.
      cancel_future_appointments(@courses, @user_ids)

View on GitHub (pinned to 1c9f0bb801)