{"title":"Background jobs","description":"Define, prioritize, retry, schedule, and safely capacity-plan Amber V2 background jobs","section":"guides","version":"v2","path":"guides/background-jobs","canonical_url":"https://amberframework.org/docs/v2/guides/background-jobs","markdown_url":"https://amberframework.org/docs/v2/guides/background-jobs.md","inherited":false,"content_markdown":"# Background jobs\n\nAmber V2 can move slow work out of an HTTP request without adding a job library.\nJobs serialize their arguments, enter a named queue, and run in worker fibers.\nThe built-in adapter is intentionally small; understand its durability and\nmemory boundary before using it in production.\n\n## 1. Define and register a job\n\n**File: `src/jobs/build_report_job.cr` — create this file.**\n\n```crystal\nclass BuildReportJob < Amber::Jobs::Job\n  include JSON::Serializable\n\n  property report_id : Int64\n\n  def initialize(@report_id : Int64)\n  end\n\n  def perform\n    ReportBuilder.build(report_id)\n  end\n\n  def self.queue : String\n    \"reports\"\n  end\n\n  def self.max_retries : Int32\n    5\n  end\nend\n\nAmber::Jobs.register(BuildReportJob)\n```\n\n**File: `src/my_app.cr` — require jobs before the server starts.**\n\n```crystal\nrequire \"./jobs/**\"\nrequire \"../config/routes\"\n```\n\nReplace `my_app` with the generated application filename. Registration is\nrequired because a worker must reconstruct the typed job from its JSON payload.\n\n## 2. Enqueue from the request boundary\n\n**File: `src/controllers/reports_controller.cr` — enqueue only after request\nvalidation and persistence succeed.**\n\n```crystal\nclass ReportsController < ApplicationController\n  def create\n    report = ReportCatalog.create(params)\n    BuildReportJob.new(report.id).enqueue\n\n    redirect_to \"/reports/#{report.id}\"\n  end\nend\n```\n\nUse `enqueue(delay: 5.minutes)` for delayed work or\n`enqueue(queue: \"critical\")` for a one-off queue override.\n\n## 3. Configure workers and queue priority\n\n**File: `config/environments/development.yml` — add this top-level block for a\nlocal, single-process application.**\n\n```yaml\njobs:\n  adapter: \"memory\"\n  workers: 2\n  auto_start: true\n  polling_interval_seconds: 1.0\n  scheduler_interval_seconds: 5.0\n  work_stealing: false\n```\n\n**File: `config/application.cr` — set ordered queues when the application needs\nmore than `default`.**\n\n```crystal\nAmber::Jobs.configure do |config|\n  config.queues = [\"critical\", \"default\", \"reports\", \"low\"]\nend\n```\n\nWorkers check this list from left to right and take the first available job.\nThis is strict queue ordering, not weighted fairness: a continuously full\n`critical` queue can starve the queues after it.\n\n## Retries and dead jobs\n\nEach execution increments the envelope's attempt count. A failure is scheduled\nagain with exponential backoff; after `max_retries`, the adapter marks the job\ndead. The in-memory adapter exposes completed, failed, scheduled, and dead job\ncollections for inspection, but Amber V2 does not yet ship a dashboard or a\ndurable replay policy.\n\nKeep job bodies idempotent. A worker can fail after an external side effect but\nbefore completion is recorded, so an adapter that promises delivery may run the\nsame logical job again.\n\n## What request-aware work stealing means\n\n**Beta.3 behavior:** work stealing remains off by default. When enabled, Amber\nstarts one additional idle-only worker. Amber's outer\nrequest pipeline increments a live counter for each ordinary HTTP request and\ndecrements it in an `ensure` block. The idle-only worker dequeues a job only\nwhen that counter is zero. Upgraded WebSocket connections are excluded so one\npersistent connection does not disable idle work forever.\n\nThis is a conservative scheduling signal, not CPU or memory telemetry. A job\nalready running is allowed to finish, and Amber does not preempt it when a new\nrequest arrives. Keep latency-sensitive production workers separate until the\napplication has measured its own job duration and request tail latency.\n\n## Memory, durability, and multiple instances\n\nThe default `memory` adapter is:\n\n- process-local and lost on restart;\n- unbounded by the framework, so queued payloads consume application memory;\n- unavailable to workers in another process;\n- appropriate for development, tests, and deliberately small single-process\n  deployments where those limits are acceptable.\n\nFor durable or multi-instance work, implement and register a `QueueAdapter`\nbacked by a service with explicit queue-size, payload-size, retention, timeout,\nand retry policies. Do not increase worker count as a substitute for measuring\njob memory. Start with one worker, record peak resident memory and p95 job time,\nthen raise concurrency within the smallest deployment target's headroom.\n\n## Broadcast completion to the page\n\n**File: `src/jobs/build_report_job.cr` — add the broadcast after the report is\nsuccessfully written.**\n\n```crystal\ndef perform\n  ReportBuilder.build(report_id)\n  StatusChannel.broadcast_to(\n    \"status:reports\",\n    \"report:ready\",\n    {\"id\" => report_id.to_s}\n  )\nend\n```\n\nThe [WebSockets and live pages](../websockets/) guide shows the channel, socket,\nroute, and exact browser module that receives this event."}