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:
- Oban queue configuration — defining task queues with different priorities
- Email sending integration — using Swoosh + SMTP
- Worker implementation — defining background task processing logic
- Task triggering — inserting async tasks in business operations
- Testing methods — using Oban.Testing for unit tests
Complete example code: oban_demo