# How Plausible Implements Async Jobs and Background Processing Using Oban

> Discover how Plausible leverages Oban for async jobs and background processing. Explore its PostgreSQL queues, worker modules, and cron scheduling for efficient operations.

- Repository: [Plausible Analytics/analytics](https://github.com/plausible/analytics)
- Tags: internals
- Published: 2026-05-19

---

**Plausible relies on the Oban job-processing library to execute all async jobs and background processing via PostgreSQL-backed queues, supervised worker modules, and built-in plugins for cron scheduling and maintenance.**

The Plausible analytics application handles long-running tasks—such as CSV data imports, weekly email reports, and CDN cache purging—without blocking web request processes. According to the `plausible/analytics` source code, the architecture centers on Oban, an Elixir library that persists jobs to PostgreSQL and processes them through a supervised supervision tree. This design provides durability, automatic retries, and distributed processing capabilities across multiple nodes.

## Oban Configuration and Database Architecture

The foundation of Plausible's async jobs and background processing is defined in [`config/runtime.exs`](https://github.com/plausible/analytics/blob/main/config/runtime.exs). In production environments (`:prod`, `:ce`, `:load`), the configuration activates Oban with specific plugins and queue definitions.

The setup includes four essential plugins:

- **Pruner** – Automatically deletes completed jobs older than 30 days to prevent table bloat
- **Cron** – Executes scheduled tasks using crontab expressions (e.g., weekly email reports)
- **Lifeline** – Rescues orphaned jobs that have been stuck for 2 hours
- **Reindexer** – Rebuilds the job index nightly to maintain query performance

The configuration also declares dedicated queues for different workload types, including `:analytics_imports`, `:schedule_email_reports`, and `:spike_notifications`. When Cron is enabled, Plausible activates a Postgres-based peer mechanism for distributed leader election across multiple nodes.

Source: [[`config/runtime.exs`](https://github.com/plausible/analytics/blob/main/config/runtime.exs)](https://github.com/plausible/analytics/blob/master/config/runtime.exs#L63-L77)

All jobs are stored in the `oban_jobs` table, created by the migration [`20200529071028_add_oban_jobs_table.exs`](https://github.com/plausible/analytics/blob/main/20200529071028_add_oban_jobs_table.exs). This PostgreSQL-backed storage ensures that jobs survive application restarts and allows for transactional job enqueueing alongside business data changes.

## Worker Module Structure

Each background task in Plausible is implemented as an **Oban.Worker** module located under `lib/workers/`. Workers declare their target queue, retry policies, uniqueness constraints, and implement a `perform/1` callback that receives an `%Oban.Job{}` struct.

The `ImportAnalytics` worker demonstrates complex job constraints for processing CSV and GA4 imports:

```elixir
use Oban.Worker, 
  queue: :analytics_imports, 
  max_attempts: 3,
  unique: [fields: [:args], keys: [:import_id], period: 60]

```

Source: [[`lib/workers/import_analytics.ex`](https://github.com/plausible/analytics/blob/main/lib/workers/import_analytics.ex)](https://github.com/plausible/analytics/blob/master/lib/workers/import_analytics.ex#L9-L14)

The `ScheduleEmailReports` worker handles cron-driven scheduling with a simpler configuration:

```elixir
use Oban.Worker, 
  queue: :schedule_email_reports

```

Source: [[`lib/workers/schedule_email_reports.ex`](https://github.com/plausible/analytics/blob/main/lib/workers/schedule_email_reports.ex)](https://github.com/plausible/analytics/blob/master/lib/workers/schedule_email_reports.ex#L1-L4)

For tasks requiring specific retry behavior, the `PurgeCDNCache` worker implements custom exponential back-off:

```elixir
@impl Oban.Worker
def backoff(%Oban.Job{attempt: attempt}) do
  # Back-off: 3 min, 6 min, 12 min, etc.

  :math.pow(2, attempt - 1) * 180 |> round()
end

```

Source: [[`lib/workers/purge_cdn_cache.ex`](https://github.com/plausible/analytics/blob/main/lib/workers/purge_cdn_cache.ex)](https://github.com/plausible/analytics/blob/master/lib/workers/purge_cdn_cache.ex#L33-L44)

## Job Enqueueing and Execution Patterns

Application code enqueues async jobs by calling `Oban.insert!/1` or using the `.new/1` helper generated by the `Oban.Worker` macro. This persists the job to the `oban_jobs` table where it becomes available for Oban's supervisor processes to pick up and execute in the background.

A typical enqueueing pattern for scheduling email reports looks like this:

```elixir
Plausible.Workers.SendEmailReport.new(
  %{site_id: site.id, interval: "weekly"},
  scheduled_at: Plausible.Workers.ScheduleEmailReports.monday_9am(site.timezone)
)
|> Oban.insert!()

```

Source: [[`lib/workers/schedule_email_reports.ex`](https://github.com/plausible/analytics/blob/main/lib/workers/schedule_email_reports.ex)](https://github.com/plausible/analytics/blob/master/lib/workers/schedule_email_reports.ex#L41-L46)

When the application boots, Oban reads the runtime configuration, creates the specified queues, and starts its supervision tree. Workers then poll their respective queues, respecting uniqueness constraints and respecting the `max_attempts` limits defined in their module configuration.

## Code Examples for Common Async Tasks

### Enqueueing One-Shot Jobs

To enqueue a trial notification email from any application module:

```elixir
Oban.insert!(
  %Oban.Job{
    queue: :send_trial_notifications,
    worker: "Plausible.Workers.SendTrialNotifications",
    args: %{"user_id" => user.id}
  }
)

```

*Implementation:* [[`lib/workers/send_trial_notifications.ex`](https://github.com/plausible/analytics/blob/main/lib/workers/send_trial_notifications.ex)](https://github.com/plausible/analytics/blob/master/lib/workers/send_trial_notifications.ex)

### Creating a Custom Worker with Uniqueness

A complete worker implementation with custom back-off logic for CDN cache purging:

```elixir
defmodule Plausible.Workers.PurgeCDNCache do
  use Oban.Worker,
    queue: :purge_cdn_cache,
    max_attempts: 5,
    unique: [period: 3600, keys: [:id]]

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"id" => id}}) do
    # Execute cache purge logic

    :ok
  end

  @impl Oban.Worker
  def backoff(%Oban.Job{attempt: attempt}) do
    :math.pow(2, attempt - 1) * 180 |> round()
  end
end

```

Source: [[`lib/workers/purge_cdn_cache.ex`](https://github.com/plausible/analytics/blob/main/lib/workers/purge_cdn_cache.ex)](https://github.com/plausible/analytics/blob/master/lib/workers/purge_cdn_cache.ex)

### Configuring Recurring Cron Jobs

Recurring tasks are defined in [`config/runtime.exs`](https://github.com/plausible/analytics/blob/main/config/runtime.exs) using the Cron plugin. The following configuration schedules weekly email reports every Monday at 9 AM:

```elixir
config :plausible, Oban,
  plugins: [
    {Oban.Plugins.Cron,
     crontab: [
       {"0 9 * * MON", Plausible.Workers.ScheduleEmailReports, args: %{}}
     ]}
  ]

```

The `ScheduleEmailReports` worker then queries for active sites and enqueues individual `SendEmailReport` jobs for each recipient.

Source: [[`config/runtime.exs`](https://github.com/plausible/analytics/blob/main/config/runtime.exs)](https://github.com/plausible/analytics/blob/master/config/runtime.exs#L68-L70)

## Key Implementation Files

- **Oban Configuration** – [[`config/runtime.exs`](https://github.com/plausible/analytics/blob/main/config/runtime.exs)](https://github.com/plausible/analytics/blob/master/config/runtime.exs) defines queues, plugins, and distributed processing settings
- **Database Migration** – [[`priv/repo/migrations/20200529071028_add_oban_jobs_table.exs`](https://github.com/plausible/analytics/blob/main/priv/repo/migrations/20200529071028_add_oban_jobs_table.exs)](https://github.com/plausible/analytics/blob/master/priv/repo/migrations/20200529071028_add_oban_jobs_table.exs) creates the `oban_jobs` table
- **Import Processing** – [[`lib/workers/import_analytics.ex`](https://github.com/plausible/analytics/blob/main/lib/workers/import_analytics.ex)](https://github.com/plausible/analytics/blob/master/lib/workers/import_analytics.ex) handles CSV/GA4 imports with uniqueness constraints
- **Email Scheduling** – [[`lib/workers/schedule_email_reports.ex`](https://github.com/plausible/analytics/blob/main/lib/workers/schedule_email_reports.ex)](https://github.com/plausible/analytics/blob/master/lib/workers/schedule_email_reports.ex) demonstrates cron-driven batch job creation
- **CDN Management** – [[`lib/workers/purge_cdn_cache.ex`](https://github.com/plausible/analytics/blob/main/lib/workers/purge_cdn_cache.ex)](https://github.com/plausible/analytics/blob/master/lib/workers/purge_cdn_cache.ex) shows custom retry logic implementation
- **Testing Utilities** – `test/workers/*_test.exs` files demonstrate `Oban.Testing` patterns for asserting job insertion and execution

## Summary

- Plausible implements async jobs and background processing using **Oban**, a PostgreSQL-backed job queue for Elixir that stores all jobs in the `oban_jobs` table.
- Workers are defined as modules under `lib/workers/` using the `Oban.Worker` behaviour, specifying queues, retry limits, and uniqueness constraints.
- **Four plugins** handle maintenance automatically: Pruner (30-day retention), Cron (scheduled execution), Lifeline (orphan rescue), and Reindexer (nightly optimization).
- Jobs are enqueued via `Oban.insert!/1` and executed by supervised processes that respect custom back-off strategies and transactional guarantees.
- The architecture supports distributed processing through a Postgres-based peer system for environments running multiple nodes.

## Frequently Asked Questions

### What job processing library does Plausible use for async tasks?

Plausible uses **Oban**, an Elixir library that stores background jobs in PostgreSQL. This provides ACID guarantees, allowing jobs to be enqueued within the same database transaction as business data changes, ensuring data consistency even if the application crashes.

### How does Plausible configure recurring background jobs?

Recurring jobs are configured in [`config/runtime.exs`](https://github.com/plausible/analytics/blob/main/config/runtime.exs) using the `Oban.Plugins.Cron` plugin with crontab expressions. For example, weekly email reports run at 9 AM every Monday via the `ScheduleEmailReports` worker, which then enqueues individual email jobs for each site.

### How are failed async jobs handled and retried?

Failed jobs are automatically retried based on the `max_attempts` value configured in each worker module (typically 3-5 attempts). Workers can implement custom `backoff/1` functions, such as the exponential back-off in `PurgeCDNCache`, which uses `:math.pow(2, attempt - 1) * 180` to calculate delay times in seconds.

### Where does Plausible store background job data?

All async jobs are persisted to the **`oban_jobs`** table in PostgreSQL, created by the migration [`20200529071028_add_oban_jobs_table.exs`](https://github.com/plausible/analytics/blob/main/20200529071028_add_oban_jobs_table.exs). This table stores job arguments, queue assignments, attempt counts, and timestamps, enabling durability and visibility into job processing status.