Background Job Processing with Oban


In web applications, time-consuming operations like email sending and batch data processing can severely impact user experience and response time if they block the main thread. This article introduces how to use Oban to handle these background tasks asynchronously, using a welcome email sent on user creation as a complete example.

Requirements

Dependency Version
PostgreSQL 16.8+
Erlang 27.2+
Elixir 1.18+
Phoenix 1.17+
Oban 2.19+
Cookiecutter 2.6+

Initialize Project

cookiecutter git@github.com:seangong0/cookiecutter-phoenix.git \
    --no-input \
    app_name=oban_demo
cd oban_demo
mix setup

Oban Queue Configuration

Configure Oban in config/config.exs:

config :oban_demo, Oban,
  engine: Oban.Engines.Basic,
  queues: [
    # Default queue, concurrency 10
    default: 10,
    # Email queue, concurrency 5
    emails: 5,
    # High priority queue, concurrency 1
    critical: 1
  ],
  repo: ObanDemo.Repo,
  prefix: "oban"

Configuration notes:

  • engine: Uses Basic engine, suitable for single-node applications
  • queues: Defines queues and concurrency; lower numbers indicate higher priority
  • prefix: Table prefix, supports multi-tenant scenarios

Create User Context

mix phx.gen.context Accounts User users name:string email:string:unique

Modify lib/oban_demo/accounts/user.ex to inherit ObanDemo.Schema:

use ObanDemo.Schema

Configure Test Email Service

We use Mailtrap’s SMTP service to test email sending.

Register and Get Credentials

After registering, get the SMTP configuration from Email Testing -> Inboxes -> Integration:

Config Value
Host sandbox.smtp.mailtrap.io
Port 25, 465, 587 or 2525
Username Get from Mailtrap
Password Get from Mailtrap
Auth PLAIN, LOGIN, CRAM-MD5
TLS Optional (all ports support STARTTLS)

Add Dependencies

# mix.exs
defp deps do
  [
    {:dotenvy, "~> 1.0.1"},
    {:swoosh, "~> 1.18.2"},
    {:gen_smtp, "~> 1.2.0"}
  ]
end

Configure Email Sending

config/runtime.exs:

import Dotenvy

if config_env() != :test do
  env_dir_prefix = System.get_env("RELEASE_ROOT") || Path.expand("./")

  source!([
    Path.absname(".env", env_dir_prefix),
    System.get_env()
  ])

  config :oban_demo, ObanDemo.Mailer,
    adapter: Swoosh.Adapters.SMTP,
    relay: env!("EMAIL_HOST", :string!),
    port: env!("EMAIL_PORT", :integer!),
    username: env!("EMAIL_USERNAME", :string!),
    password: env!("EMAIL_PASSWORD", :string!),
    ssl: :if_available,
    tls: :never,
    auth: :always,
    retries: 2,
    no_mx_lookup: false
end

lib/oban_demo/mailer.ex:

defmodule ObanDemo.Mailer do
  use Swoosh.Mailer, otp_app: :oban_demo
end

Define Email Template

lib/oban_demo/user_email.ex:

defmodule ObanDemo.UserEmail do
  import Swoosh.Email

  def welcome(user) do
    new()
    |> to({user.name, user.email})
    |> from({"Oban Demo", "noreply@example.com"})
    |> subject("Welcome to Oban Demo")
    |> html_body("<h1>Hello #{user.name}</h1>")
    |> text_body("Hello #{user.name}\n")
  end
end

Create an Oban Worker

lib/oban_demo/workers/email_worker.ex:

defmodule ObanDemo.Workers.EmailWorker do
  require Logger
  use Oban.Worker, queue: :emails, max_attempts: 3

  alias ObanDemo.Accounts
  alias ObanDemo.Mailer
  alias ObanDemo.UserEmail

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"user_id" => user_id}}) do
    case Accounts.get_user(user_id) do
      nil ->
        {:error, "User #{user_id} not found"}

      user ->
        send_welcome_email(user)
    end
  end

  defp send_welcome_email(user) do
    user
    |> UserEmail.welcome()
    |> Mailer.deliver()
    |> handle_email_delivery()
  end

  defp handle_email_delivery({:ok, _}), do: :ok

  defp handle_email_delivery({:error, reason}) do
    Logger.error("Email sending failed: #{inspect(reason)}")
    {:error, reason}
  end
end

Parameter notes:

  • queue: Specifies the job queue name
  • max_attempts: Maximum retry attempts; failures enter the retry queue

.env Configuration Example

EMAIL_HOST=sandbox.smtp.mailtrap.io
EMAIL_PORT=2525
EMAIL_USERNAME=your_username
EMAIL_PASSWORD=your_password

Trigger Email on User Creation

lib/oban_demo/accounts.ex:

def create_user(attrs \\ %{}) do
  %User{}
  |> User.changeset(attrs)
  |> Repo.insert()
  |> case do
    {:ok, user} ->
      # Insert Oban job asynchronously
      %{"user_id" => user.id}
      |> ObanDemo.Workers.EmailWorker.new()
      |> Oban.insert()

      {:ok, user}

    {:error, changeset} ->
      {:error, changeset}
  end
end

Testing

# Start the server
iex -S mix phx.server

# Create a user, triggering email sending
ObanDemo.Accounts.create_user(%{name: "test", email: "test@example.com"})

After execution, log into Mailtrap to check if the welcome email was received.

Write Worker Tests

test/oban_demo/workers/email_worker_test.exs:

defmodule ObanDemo.Workers.EmailWorkerTest do
  use ObanDemo.DataCase, async: true
  use Oban.Testing, repo: ObanDemo.Repo

  alias ObanDemo.Workers.EmailWorker

  test "sends welcome email" do
    {:ok, %{id: user_id}} =
      ObanDemo.Accounts.create_user(%{name: "test", email: "test@example.com"})

    assert :ok = perform_job(EmailWorker, %{"user_id" => user_id})
  end

  test "returns error when user not found" do
    fake_user_id = Ecto.UUID.generate()

    assert {:error, reason} = perform_job(EmailWorker, %{"user_id" => fake_user_id})
    assert reason =~ "User #{fake_user_id} not found"
  end
end

Run the tests:

mix test test/oban_demo/workers/email_worker_test.exs

Summary

This article covered:

  1. Oban queue configuration — defining task queues with different priorities
  2. Email sending integration — using Swoosh + SMTP
  3. Worker implementation — defining background task processing logic
  4. Task triggering — inserting async tasks in business operations
  5. Testing methods — using Oban.Testing for unit tests

Complete example code: oban_demo