activejob-later

Run any method of any class, module or instance in the background through Active Job — no dedicated job class needed.

Report.perform_later.compute("2026-06")
Report.perform_later(queue: :low, wait: 10.minutes).compute("2026-06")
invoice.perform_later.generate_pdf          # instances via GlobalID
Maintenance.perform_later.purge             # modules too

The direct call (Report.compute(...)) stays synchronous and untouched. Options are passed straight to ActiveJob::Base.set (queue:, wait:, wait_until:, priority:).

Per-class and per-method job configuration

perform_later calls go through a generic job by default. Declare a dedicated job to use the full Active Job class DSL — retry_on, discard_on, rescue_from, queue_as, callbacks, and adapter extensions such as limits_concurrency (Solid Queue):

class Report
  perform_later_job do                      # applies to the whole class
    queue_as :reports
    retry_on Timeout::Error, attempts: 5
  end

  perform_later_job :compute do             # applies to one method only
    limits_concurrency to: 1, key: ->(target, *) { target }
    discard_on ActiveRecord::RecordNotFound
  end

  def self.compute(month)
    # ...
  end
end

Job resolution order: method job (Report::ComputePerformLaterJob) → class job (Report::PerformLaterJob) → generic (ActiveJob::PerformLater::Job). Method jobs inherit the class job, so rules cumulate. Declare the class-level perform_later_job before the per-method ones — a method job only inherits a class job that already exists at that point.

When a class method and an instance method share a name, scope the job:

perform_later_job :sync, on: :class do ... end      # Report.perform_later.sync
perform_later_job :sync, on: :instance do ... end   # report.perform_later.sync

Building a job without enqueuing it

job_later takes the same options and returns the job perform_later would enqueue, without enqueuing it — for APIs that take a job instance, such as Solid Queue batch callbacks:

SolidQueue::Batch.enqueue(on_finish: import.job_later.finalize!) do
  import.lines.each { |line| line.perform_later.process! }
end

Job resolution and the errors below are the same as for perform_later.

Configuration

ActiveJob::PerformLater.base_job = "ApplicationJob"  # default: "ActiveJob::Base"

Parent class of the jobs generated by perform_later_job.

Error behavior

  • Unknown or private method → NoMethodError at enqueue time, not in the worker.
  • Non-serializable instance (no GlobalID, no custom serializer) → ActiveJob::SerializationError at enqueue time.
  • Target class removed before the job runs → NameError at perform.
  • The proxy forwards every method call, so reflective calls like .class on the proxy itself enqueue real jobs; inspect is the exception (safe to display in a console).
  • String instances are rejected as targets (ArgumentError) — a String would be ambiguous with a class reference in the job payload.
  • Passing a block to a proxied call raises ArgumentError — blocks cannot be serialized.

Requirements

Ruby >= 3.3, activejob >= 7.1.