bitwarden/server
Jobs & scheduling (Quartz.NET)
Active contributors: per-domain — each team adds its own jobs.
Purpose
Bitwarden uses Quartz.NET for scheduled background work. Each app that runs jobs registers a JobsHostedService in its Startup, which spins up a Quartz scheduler, builds the job triggers, and lets IServiceCollection resolve the job implementations through DI.
Where jobs live
| App | JobsHostedService | Typical jobs |
|---|---|---|
Api |
src/Api/Jobs/JobsHostedService.cs |
Email-token cleanup, organization domain verification, user account-revision pruning, send cleanup, alive ping. Plus Secrets-Manager jobs registered conditionally for non-OSS builds. |
Admin |
src/Admin/HostedServices/ + src/Admin/Jobs/ |
License refresh, Sponsorship cleanup, periodic block-list refresh. |
Billing |
src/Billing/Jobs/ |
Stripe alive ping, retry of dropped events, periodic plan-mismatch reconciliation. |
Notifications |
src/Notifications/Jobs/ |
Stale-connection cleanup, periodic stats roll-up. |
Sso (commercial) |
bitwarden_license/src/Sso/Jobs/ |
OIDC discovery refresh. |
Job structure
Each job inherits from a small BaseJob (or IJob directly):
public class EmergencyAccessNotificationJob : BaseJob
{
private readonly IEmergencyAccessService _service;
public EmergencyAccessNotificationJob(IEmergencyAccessService s,
ILogger<EmergencyAccessNotificationJob> logger) : base(logger) { _service = s; }
protected override async Task ExecuteJobAsync(IJobExecutionContext context)
=> await _service.SendNotificationsAsync();
}Triggers are built in JobsHostedService.RegisterJobsAsync using cron expressions:
new Job(typeof(EmergencyAccessNotificationJob),
"0 0 6 * * ?", // every day at 06:00 UTC
trigger: ...);Quartz schedules in-memory by default (the cloud setup also scales horizontally — Quartz is configured per-pod, jobs that must singleton-execute use database advisory locks).
Running modes
JobsHostedService.AddJobsServices(services, globalSettings.SelfHosted) decides which jobs to register based on globalSettings.SelfHosted. Some are cloud-only (e.g. organization domain DNS verification), some are self-host-only.
Self-host
Each container that includes JobsHostedService runs jobs locally. There is no central scheduler. The compose stack relies on the fact that there's only one instance of each service, so cron-style fan-out doesn't double-fire.
Entry points for modification
- New job → write the class under
src/Api/Jobs/,src/Admin/Jobs/, or wherever it belongs; register it inJobsHostedService.AddJobsServices(or the equivalentAddCommercialSecretsManagerJobServicesfor licensed jobs). - Tweak schedule → adjust the cron expression in the registration call.
- Add a job that needs locking across replicas → wrap the operation in a Postgres advisory lock or
dbo.Migration-style table guard. There is no built-in Quartz JobStore.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.