How Plausible Implements Async Jobs and Background Processing Using Oban
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. 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/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. 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:
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/master/lib/workers/import_analytics.ex#L9-L14)
The ScheduleEmailReports worker handles cron-driven scheduling with a simpler configuration:
use Oban.Worker,
queue: :schedule_email_reports
Source: [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:
@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/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:
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/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:
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/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:
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/master/lib/workers/purge_cdn_cache.ex)
Configuring Recurring Cron Jobs
Recurring tasks are defined in config/runtime.exs using the Cron plugin. The following configuration schedules weekly email reports every Monday at 9 AM:
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/master/config/runtime.exs#L68-L70)
Key Implementation Files
- Oban Configuration – [
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/master/priv/repo/migrations/20200529071028_add_oban_jobs_table.exs) creates theoban_jobstable - Import Processing – [
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/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/master/lib/workers/purge_cdn_cache.ex) shows custom retry logic implementation - Testing Utilities –
test/workers/*_test.exsfiles demonstrateOban.Testingpatterns 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_jobstable. - Workers are defined as modules under
lib/workers/using theOban.Workerbehaviour, 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!/1and 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 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. This table stores job arguments, queue assignments, attempt counts, and timestamps, enabling durability and visibility into job processing status.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →