Skip to main content
Signal’s job system provides durable, retryable background work that survives process death. Almost all network activity — sending messages, uploading attachments, processing incoming push notifications — runs through it. The core classes live in jobmanager/ and jobs/.

Overview

JobManager is initialized once and accessible via AppDependencies.getJobManager(). All job submission goes through it.

The Job base class

Every job extends Job (or the convenience subclass BaseJob). A job is a durable unit of work described by Job.Parameters:
BaseJob adds the onRun() / onShouldRetry() pattern and handles the Result dispatch:

Job lifecycle

1

Submission

The caller creates a job and calls JobManager.add(job) or JobManager.startChain(job).then(nextJob).enqueue(). The job is immediately persisted to FastJobStorage and enqueued.
2

Constraint check

Before a job runner picks up the job, all declared constraints must be satisfied (e.g., network available). If constraints are not met, the job stays in the queue until a ConstraintObserver fires.
3

onAdded()

Called on the worker thread as soon as the job enters the queue. Useful for optimistic local state updates (e.g., inserting a pending message into the database).
4

run()

The job’s main work is executed. Returns Result.success(), Result.retry(backoffMs), or Result.failure().
5

Success

Result.success() — the job is removed from storage. If there is a chain, the next job becomes eligible.
6

Retry

Result.retry(backoffMs) — the job remains in storage with an updated nextBackoffInterval. It becomes eligible again after the backoff has elapsed. BaseJob.getNextRunAttemptBackoff() defaults to exponential backoff via BackoffUtil.
7

Failure

Result.failure() — onFailure() is called. Dependent jobs in the same chain are also failed (cascading failure). The job is removed from storage.

Result types

Job Parameters

Job.Parameters controls scheduling behavior:

Constraints

A Constraint is a condition that must be true before a job can run. All built-in constraints live in jobmanager/impl/: Each Constraint implementation has a corresponding ConstraintObserver that monitors system state and notifies JobManager when the constraint transitions to met, allowing waiting jobs to be picked up immediately. To declare a constraint on a job:

Job queues and chaining

Jobs in the same queue run serially — a queue acts as a mutual exclusion key. Jobs in different queues (or with no queue) run concurrently up to the configured maxGeneralRunners limit. Chaining ensures that a sequence of jobs runs in order, and that a failure in one job propagates forward:
Output data from a completed job is passed to the next job(s) in the chain via setOutputData(byte[]) / getInputData().

FastJobStorage

FastJobStorage is the primary JobStorage implementation. It keeps the full job queue in memory for fast access while using JobDatabase (a separate SQLite database from signal.db) for persistence:
JobSpec (the persisted form) captures all scheduling state:
Memory-only jobs (isMemoryOnly = true) never touch JobDatabase and are lost on process death. They are suitable for best-effort work that is acceptable to drop.

Key job types

IndividualSendJob

Sends an encrypted message to a single recipient. Usually the second step in a chain after AttachmentUploadJob. Handles retry on network errors and re-encryption on session reset.

GroupSendJob

Sends a message to a group. Fans out to individual sends and manages sender key distribution. Handles the group-send-endorsement flow.

AttachmentUploadJob

Uploads an attachment to Signal’s CDN and records the remote pointer in AttachmentTable. Runs before any send job that requires an attachment.

PushProcessMessageJob

Processes a single decrypted envelope received from the Signal server. Handles message insertion, receipts, group updates, story syncs, and call event processing.

PreKeysSyncJob

Generates new one-time prekeys and/or rotates signed prekeys when the supply on the server is running low. Runs on a DecryptionsDrainedConstraint to avoid racing with message processing.

RefreshAttributesJob

Re-registers the device’s attributes (capabilities, name, profile key) with the Signal server after they change.

Adding a new job

1

Create the job class

Extend BaseJob in the jobs/ package. Declare a KEY constant for the factory and implement onRun(), onShouldRetry(), onFailure(), and serialize() / deserialize().
2

Register the factory

Add an entry for MyNewJob.KEY → new MyNewJob.Factory() in the JobManager.Configuration setup inside AppDependencies (look for the setJobFactories() call).
3

Submit the job

Call AppDependencies.getJobManager().add(new MyNewJob()) wherever you want to trigger the work.
Never perform network I/O or long-running work directly in onAdded(). That method is called on a queue-management thread. Use it only for fast local state updates.