Background Jobs & Scheduler
Heimdall runs recurring background work through the heimdall-scheduler crate. It
registers a small set of cron jobs on a tokio_cron_scheduler
JobScheduler and starts them when the API boots.
Overview
Location: crates/heimdall-scheduler/
The scheduler is started via start_scheduler(...), which:
- Checks whether the scheduler is enabled (default: enabled).
- Reads each job's cron expression and tuning values from configuration, falling back to built-in defaults when unset.
- Registers the jobs on a
JobScheduler. - Runs an initial scheduled-deletion check on startup so it does not have to wait for the first cron interval.
- Starts the scheduler.
If the scheduler is disabled, start_scheduler returns an empty JobScheduler
with no jobs registered.
All cron expressions use the 6-field tokio_cron_scheduler format:
sec min hour day month day-of-week.
Jobs
| Job | Default schedule | What it does | Config keys |
|---|---|---|---|
| Scheduled deletion processing | 0 */15 * * * * (every 15 min) | Anonymizes user accounts whose grace period has elapsed | scheduler.deletion_cron |
| Integration token refresh | 0 */5 * * * * (every 5 min) | Proactively refreshes OAuth tokens expiring soon | scheduler.integration_refresh_cron, scheduler.integration_refresh_buffer_minutes |
| Channel stats refresh | 0 0 */6 * * * (every 6 hours) | Refreshes follower/subscriber counts for stale channel integrations | scheduler.channel_stats_refresh_cron, scheduler.channel_stats_stale_minutes |
The whole scheduler can be turned off with scheduler.enabled = false.
Scheduled deletion processing
Implemented in process_scheduled_deletions. It selects rows from
ScheduledDeletion joined to User where delete_at <= NOW() and the user is
not already deleted (u.deleted_at IS NULL), ordered by delete_at ascending. For
each match it calls anonymize_user (full data cleanup, including TimescaleDB audit
anonymization and Redis cache invalidation) and broadcasts an account-deletion event
over WebSocket to force-logout active sessions. Failures are logged per-user and the
job continues with the remaining users. The job returns the count of successfully
processed deletions.
This job also runs once on startup before the cron loop begins.
See GDPR & Privacy for the account-deletion grace-period flow
that creates the ScheduledDeletion rows this job consumes.
Integration token refresh
Implemented in refresh_expiring_tokens. It loads integrations whose OAuth tokens
expire within the configured buffer (get_integrations_needing_refresh). For each
one it decrypts the stored refresh token, calls the platform's OAuth refresh
endpoint, re-encrypts the new access/refresh tokens, and updates the integration's
stored tokens and expires_at.
- Integrations without a refresh token, or with an unknown platform, are skipped.
- Successful refreshes log a
integration_token_refreshedaudit event (with no user/IP, since it is an automatic background refresh). - Failures mark the integration with an error state and log a
integration_token_erroraudit event.
The buffer (minutes before expiry to trigger a refresh) defaults to 15 and is
controlled by scheduler.integration_refresh_buffer_minutes.
Channel stats refresh
Implemented in refresh_channel_stats. It loads channel-type integrations that
have not had their stats updated within the stale threshold
(get_channel_integrations_for_stats_refresh). For each one it decrypts the access
token and fetches channel statistics from the platform API, then upserts the
follower/subscriber counts.
- Only Twitch and YouTube are supported; other platforms are skipped.
- The stale threshold defaults to 360 minutes (6 hours) and is controlled by
scheduler.channel_stats_stale_minutes.
Configuration
Scheduler settings live under the [scheduler] section. All cron and tuning values
are optional and fall back to the defaults shown above.
[scheduler]
# Enable/disable the background job scheduler (default: true)
enabled = true
# Cron for account deletion processing (default: "0 */15 * * * *")
# Format: sec min hour day month day-of-week
deletion_cron = "0 */15 * * * *"
# Cron for integration OAuth token refresh (default: "0 */5 * * * *")
integration_refresh_cron = "0 */5 * * * *"
# Refresh tokens this many minutes before expiry (default: 15)
integration_refresh_buffer_minutes = 15
# Cron for channel stats refresh (default: "0 0 */6 * * *")
channel_stats_refresh_cron = "0 0 */6 * * *"
# Minutes after which channel stats are considered stale (default: 360 = 6h)
channel_stats_stale_minutes = 360
| Key | Type | Default | Purpose |
|---|---|---|---|
scheduler.enabled | bool | true | Master on/off switch; when false no jobs are registered |
scheduler.deletion_cron | string | "0 */15 * * * *" | Deletion job schedule |
scheduler.integration_refresh_cron | string | "0 */5 * * * *" | Token refresh job schedule |
scheduler.integration_refresh_buffer_minutes | int | 15 | Refresh tokens expiring within this window |
scheduler.channel_stats_refresh_cron | string | "0 0 */6 * * *" | Channel stats job schedule |
scheduler.channel_stats_stale_minutes | int | 360 | Treat channel stats older than this as stale |
These values can also be overridden via environment variables (e.g.
HEIMDALL__SCHEDULER__DELETION_CRON). See Configuration for
the full configuration resolution order.
Related
- Configuration -- config resolution and environment overrides
- GDPR & Privacy -- account-deletion grace period and anonymization
- Crate Reference --
heimdall-schedulerand related crates