Vaultwarden Job Scheduler Features: Automated Cleanup for Sends, Trash, and Events

Vaultwarden includes a built-in job scheduler powered by the job_scheduler_ng crate that automatically purges expired Sends, permanently deletes trashed ciphers, and cleans old events using configurable cron expressions that run in a dedicated background thread.

Vaultwarden, the open-source Bitwarden-compatible password manager maintained by dani-garcia, provides a lightweight job scheduler that eliminates the need for external cron daemons or task runners. This system executes three distinct cleanup jobs directly within the application process, ensuring data retention policies are enforced without manual intervention. Understanding these Vaultwarden job scheduler features allows administrators to configure precise data lifecycle management through simple environment variables.

Available Cleanup Jobs

The scheduler manages three primary maintenance operations, each triggered by independent cron schedules defined in the jobs configuration section.

Purge Expired Sends

The Send purge job deletes "Send" objects that have exceeded their deletion_date, preventing indefinite storage of expired file shares. This job is controlled by the SEND_PURGE_SCHEDULE configuration key, which defaults to 0 5 * * * * (executing hourly at minute 5). In src/main.rs lines 640–645, the scheduler registers this job by spawning an async task via runtime.spawn(api::purge_sends(pool.clone())). The underlying implementation resides in src/api/core/sends.rs::purge_sends, which invokes Send::purge from src/db/models/send.rs to execute the database deletion.

Purge Trashed Ciphers

The trash purge job permanently removes ciphers that have been in the trash longer than the trash_auto_delete_days threshold. Configured via TRASH_PURGE_SCHEDULE with a default cron expression of 0 5 0 * * * (daily at 00:05), this job is registered in src/main.rs lines 647–652. When triggered, the scheduler executes api::purge_trashed_ciphers(pool.clone()), implemented in src/api/core/ciphers.rs::purge_trashed_ciphers, which calls Cipher::purge_trash in src/db/models/cipher.rs to perform the permanent deletion.

Clean Old Events

The event cleanup job purges rows from the events table that exceed the events_days_retain retention period. This job uses the EVENT_CLEANUP_SCHEDULE key, defaulting to 0 10 0 * * * (daily at 00:10), and is registered in src/main.rs lines 693–699. The execution path flows through api::event_cleanup_job in src/api/core/events.rs, which delegates to Event::clean_events in src/db/models/event.rs for the actual database cleanup operation.

Configuration and Scheduling

All cron expressions are validated at startup in src/config.rs (lines 543–548) and parsed by the job_scheduler_ng crate. An empty string for any schedule disables that specific job entirely.

Default Configuration

Enable the standard cleanup schedule using the default intervals:


# .env or config.json

JOB_POLL_INTERVAL_MS=30000
SEND_PURGE_SCHEDULE="0 5 * * * *"
TRASH_PURGE_SCHEDULE="0 5 0 * * *"
EVENT_CLEANUP_SCHEDULE="0 10 0 * * *"

Custom Intervals

Modify the cron expression to adjust frequency. For example, to purge Sends every 15 minutes instead of hourly:

SEND_PURGE_SCHEDULE="0 */15 * * * *"

Disabling Jobs

Prevent a specific job from running by setting its schedule to an empty string:


# Disable event cleanup entirely

EVENT_CLEANUP_SCHEDULE=""

Technical Implementation Details

The job scheduler architecture centers around the schedule_jobs() function in src/main.rs (spanning approximately lines 630–720), which creates a dedicated thread to host the JobScheduler instance.

Execution Model

The scheduler operates on a tick-based polling mechanism:

  1. Initialization: The thread builds a JobScheduler and registers each enabled job with its parsed cron expression
  2. Polling Loop: The thread enters an infinite loop calling sched.tick() to evaluate pending jobs
  3. Interval Control: Between ticks, the thread sleeps for JOB_POLL_INTERVAL_MS milliseconds (default 30,000ms)
  4. Async Execution: When a job triggers, the scheduler spawns the task onto the Tokio runtime with a cloned database connection pool, ensuring non-blocking operation

Database Layer Integration

Each cleanup job follows a consistent pattern of API-to-model delegation:

  • Sends: src/api/core/sends.rs::purge_sendssrc/db/models/send.rs::Send::purge
  • Ciphers: src/api/core/ciphers.rs::purge_trashed_cipherssrc/db/models/cipher.rs::Cipher::purge_trash
  • Events: src/api/core/events.rs::event_cleanup_jobsrc/db/models/event.rs::Event::clean_events

This separation ensures the scheduler thread remains lightweight while database-intensive operations execute on the async runtime with proper connection pooling.

Summary

  • Vaultwarden implements a self-contained job scheduler using the job_scheduler_ng crate, requiring no external cron service
  • Three automated jobs handle data retention: Send purge (hourly default), trash purge (daily default), and event cleanup (daily default)
  • Configuration occurs via environment variables (SEND_PURGE_SCHEDULE, TRASH_PURGE_SCHEDULE, EVENT_CLEANUP_SCHEDULE) using standard cron syntax
  • The schedule_jobs() function in src/main.rs manages a dedicated polling thread that spawns async tasks onto the Tokio runtime
  • Jobs can be individually disabled by setting their respective schedule to an empty string

Frequently Asked Questions

How do I disable a specific cleanup job in Vaultwarden?

Set the corresponding configuration key to an empty string. For example, assign EVENT_CLEANUP_SCHEDULE="" to prevent the event cleanup job from registering with the scheduler. The validation logic in src/config.rs treats empty strings as disabled jobs, and the schedule_jobs() function in src/main.rs skips registration for any job lacking a valid cron expression.

What cron format does the Vaultwarden job scheduler use?

Vaultwarden uses the standard Unix cron format with six fields (seconds, minutes, hours, days of month, months, days of week) as implemented by the job_scheduler_ng crate. The default schedules follow this pattern: 0 5 * * * * for hourly execution at minute 5, and 0 5 0 * * * for daily execution at 00:05.

How frequently does the job scheduler check for pending tasks?

The scheduler evaluates pending jobs every 30 seconds by default, controlled by the JOB_POLL_INTERVAL_MS configuration option. This polling interval determines how often the dedicated scheduler thread calls sched.tick() to check if any registered cron jobs should fire, independent of the individual job schedules themselves.

Where does the actual data deletion logic reside for purge operations?

While the scheduler triggers jobs in src/main.rs, the deletion logic resides in the database models layer. For Sends, the logic is in src/db/models/send.rs; for trashed ciphers in src/db/models/cipher.rs; and for events in src/db/models/event.rs. The API layer in src/api/core/ (sends.rs, ciphers.rs, events.rs) serves as the intermediary that the scheduler invokes, which then calls these model-specific purge methods.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →