Documentation

Background jobs

New
Browse documentation

Published 2026-08-13. V2 is a prerelease; the web core is release-gated and other previews are named separately. What beta means.

Read this page as HTML, Markdown, or structured JSON—or open the published Markdown with an AI assistant. Gemini receives the prompt through your clipboard because its signed-out page does not reliably prefill URL text; paste when the new tab opens. External assistants need the public site URL.

1.4.1
Unavailable in this version
1.5
Unavailable in this version

Background jobs

Amber V2 can move slow work out of an HTTP request without adding a job library. Jobs serialize their arguments, enter a named queue, and run in worker fibers. The built-in adapter is intentionally small; understand its durability and memory boundary before using it in production.

1. Define and register a job

File: src/jobs/build_report_job.cr — create this file.

Crystal
class BuildReportJob < Amber::Jobs::Job
  include JSON::Serializable

  property report_id : Int64

  def initialize(@report_id : Int64)
  end

  def perform
    ReportBuilder.build(report_id)
  end

  def self.queue : String
    "reports"
  end

  def self.max_retries : Int32
    5
  end
end

Amber::Jobs.register(BuildReportJob)

File: src/my_app.cr — require jobs before the server starts.

Crystal
require "./jobs/**"
require "../config/routes"

Replace my_app with the generated application filename. Registration is required because a worker must reconstruct the typed job from its JSON payload.

2. Enqueue from the request boundary

File: src/controllers/reports_controller.cr — enqueue only after request validation and persistence succeed.

Crystal
class ReportsController < ApplicationController
  def create
    report = ReportCatalog.create(params)
    BuildReportJob.new(report.id).enqueue

    redirect_to "/reports/#{report.id}"
  end
end

Use enqueue(delay: 5.minutes) for delayed work or enqueue(queue: "critical") for a one-off queue override.

3. Configure workers and queue priority

File: config/environments/development.yml — add this top-level block for a local, single-process application.

YAML
jobs:
  adapter: "memory"
  workers: 2
  auto_start: true
  polling_interval_seconds: 1.0
  scheduler_interval_seconds: 5.0
  work_stealing: false

File: config/application.cr — set ordered queues when the application needs more than default.

Crystal
Amber::Jobs.configure do |config|
  config.queues = ["critical", "default", "reports", "low"]
end

Workers check this list from left to right and take the first available job. This is strict queue ordering, not weighted fairness: a continuously full critical queue can starve the queues after it.

Retries and dead jobs

Each execution increments the envelope's attempt count. A failure is scheduled again with exponential backoff; after max_retries, the adapter marks the job dead. The in-memory adapter exposes completed, failed, scheduled, and dead job collections for inspection, but Amber V2 does not yet ship a dashboard or a durable replay policy.

Keep job bodies idempotent. A worker can fail after an external side effect but before completion is recorded, so an adapter that promises delivery may run the same logical job again.

What request-aware work stealing means

Beta.3 behavior: work stealing remains off by default. When enabled, Amber starts one additional idle-only worker. Amber's outer request pipeline increments a live counter for each ordinary HTTP request and decrements it in an ensure block. The idle-only worker dequeues a job only when that counter is zero. Upgraded WebSocket connections are excluded so one persistent connection does not disable idle work forever.

This is a conservative scheduling signal, not CPU or memory telemetry. A job already running is allowed to finish, and Amber does not preempt it when a new request arrives. Keep latency-sensitive production workers separate until the application has measured its own job duration and request tail latency.

Memory, durability, and multiple instances

The default memory adapter is:

  • process-local and lost on restart;
  • unbounded by the framework, so queued payloads consume application memory;
  • unavailable to workers in another process;
  • appropriate for development, tests, and deliberately small single-process deployments where those limits are acceptable.

For durable or multi-instance work, implement and register a QueueAdapter backed by a service with explicit queue-size, payload-size, retention, timeout, and retry policies. Do not increase worker count as a substitute for measuring job memory. Start with one worker, record peak resident memory and p95 job time, then raise concurrency within the smallest deployment target's headroom.

Broadcast completion to the page

File: src/jobs/build_report_job.cr — add the broadcast after the report is successfully written.

Crystal
def perform
  ReportBuilder.build(report_id)
  StatusChannel.broadcast_to(
    "status:reports",
    "report:ready",
    {"id" => report_id.to_s}
  )
end

The WebSockets and live pages guide shows the channel, socket, route, and exact browser module that receives this event.