Control and observe

Start and stop the queue, retry and cancel jobs, wait for it to go idle, read its state and listen to everything that happens.

Start, stop, dispose

Good reasons to stop a queue: the user signed out, a remote kill switch, or a circuit breaker after a server outage (see the recipe).

retry, remove, clear

CallOn a pending jobOn an active jobOn a failed job
retry(id)returns falsereturns falserequeued, attempts reset, deadline cleared
remove(id)deletedaborted (ctx.signal fires), then deleteddeleted
Removing a detached job

remove() deletes the job but cannot stop a native task that is already running. Cancel the native task yourself as well. See Native tasks.

flush and drain

flush() re-reads connectivity and starts everything due right now. Call it when the app returns to the foreground, because timers and network events may have been missed while it was suspended. flushOnForeground does that for you:

drain({ timeoutMs }) flushes, then waits until the queue has nothing left it can do right now. It resolves true when idle and false if the timeout ran out first.

drain does not wait for jobs that cannot run right now:

It is made for background tasks, and it is also the easiest way to write tests: await queue.drain() then assert (see Testing).

Reading state

getJobs

Without a filter, the same array is returned until something changes, so it is safe as a React snapshot. Filtered results are new arrays each call.

getStats

Also referentially stable until something changes.

subscribe

The listener runs after any change to any job, with no arguments. It is exactly the contract useSyncExternalStore needs, which is how the React hooks are built.

Events

Six moments in a job's life emit an event.

EventWhenExtra
enqueuedA job was added.
startedAn attempt began.
completedThe handler succeeded and the job was deleted. Also fired by complete(id) for detached jobs.durationMs
retryAn attempt failed and another is scheduled.error, durationMs
failedThe job gave up.error, durationMs
expiredA pending job outlived its ttlMs.
removedDeleted by remove(), clear() or retention.
requeuedConnectivity dropped mid-attempt; the job went back to pending without using an attempt.

Not every transition emits one: an offline requeue (connectivity lost mid-run) is silent, and remove() emits nothing.

Attach listeners before the queue starts

A queue created with the default autoStart: true processes saved jobs immediately, so events from that first pass (a job that expired while the app was closed, interrupted jobs starting again) fire before you can attach a listener. To see them, create the queue with autoStart: false, attach your on / observe listeners, then call queue.start().

on: one event

observe: every event, with duration

durationMs is measured for attempts run in this process. It is missing for events that are not an attempt end, and for detached jobs finished with complete() or fail().

Listener safety

An error thrown by a listener or observer is swallowed so it can never break the queue. Do your own try/catch if you need to know about it.

Connecting analytics and crash tools

observe maps directly onto any analytics SDK. Nothing in QueueKit depends on them: you attach the one you use.

Mind the payload

Payloads may hold personal data. Send ids and types to analytics, not whole payloads.

Avoid feedback loops

If you send analytics through the queue, do not enqueue a new analytics job from inside an observer for every event, or each job will create another.

Simple metrics

Debugging

A job that never runs is almost always one of: no handler registered, offline with requiresNetwork, a rate or concurrency limit, a future runAt, or a stopped queue. The checklist in the FAQ walks through each.