Background and native tasks

A JavaScript queue only runs while JavaScript runs. This page explains what that means on a phone, and the three layers QueueKit gives you to make jobs finish anyway.

What actually runs when

App stateJavaScriptThe queue
ForegroundRunsRuns normally.
Just backgroundedThe OS usually allows a short grace period (seconds)Jobs in flight may finish. New ones may not get time.
Suspended in the backgroundFrozen. Timers do not fire.Paused. Backoff timers resume when the app wakes.
Woken by the OS for a background taskRuns for a short window you do not controlYou can drain() inside it.
Terminated by the systemNot runningJobs are safe in storage and continue on next launch (or next background wake).
Force-quit by the userNot running. iOS generally will not wake the app for background tasks until the user opens it again.Waits for next launch.
Background execution is best effort

iOS and Android decide when, and whether, background work runs, based on battery, usage patterns, Low Power Mode, Doze and vendor-specific rules. Treat everything in layers 2 and 3 as "helps in practice", never as a guarantee. Layer 1 and the native tasks in layer 4 are the dependable ones.

Layer 1: catch up when the app returns

While the app was suspended, timers did not fire and connectivity may have changed without the queue noticing. flush() re-reads connectivity and starts whatever is due.

It calls queue.flush() every time the app state becomes "active". It returns a function that stops listening.

Layer 2: periodic background tasks

Both platforms can wake your app now and then to do a little work. Inside that window, await queue.drain({ timeoutMs }) runs everything it can and returns when the queue is idle or when your time budget is used up.

These snippets show the pattern; check each library's current documentation for exact option names and platform setup.

Rules of thumb

Layer 3: wake on a silent push

A silent (data-only) push notification can wake the app briefly on both platforms, again best effort. Use the same pattern in its handler:

Use it to nudge devices to flush after something changed on your server, for example a new token or a "please sync" signal.

Layer 4: native tasks with detach

For large uploads and downloads, do not run the transfer in JavaScript at all. Hand it to the OS's own transfer service, which keeps going while the app is suspended and can finish even if the app is terminated. QueueKit keeps tracking the job.

The flow

  1. The handler starts the native transfer and gets a task id.
  2. It returns ctx.detach(taskId). The job stays active with job.externalId = taskId, frees its concurrency slot and has no timeout.
  3. When the native task ends, you call queue.complete(jobId) or queue.fail(jobId, error).
  4. fail() goes through the normal retry logic: the handler runs again (a fresh upload) or the job becomes failed.
  5. After an app restart, detached jobs are still active. At startup, look them up and reattach listeners.

NativeUploader stands for whatever uploader you use (react-native-background-upload, a native module of your own, and so on). The pattern is the same: start, detach, report back.

Rules for detach

TopicBehaviour
Where to call itreturn ctx.detach(id) from a normal or async handler. It does not work from a generator handler.
Slot and timeoutThe job releases its concurrency slot and has no timeout while detached.
Statusactive, with externalId set. progress is cleared when a new attempt starts.
RestartStays active, not re-run. You reattach.
complete(id)Deletes the job and emits completed. Returns false if the job is unknown or not detached.
fail(id, error?)Retries or fails the job like a thrown error would. The default message is "External task failed".
remove(id)Deletes the job. You must also cancel the native task.
drain()Does not wait for detached jobs.
AnalyticsdurationMs is not reported for jobs finished by complete() / fail().
Finished while the app was dead

If the OS completed the transfer while your app was not running, nothing tells the queue. Make sure your uploader reports pending results on next launch, or check your backend for the object by its idempotency key, then call complete() or fail(). Do this inside reattachUploads().

Cancelling a detached upload

A sensible combination

  1. Always: flushOnForeground(queue, AppState).
  2. Small jobs (API calls, forms): rely on the queue plus a periodic background drain.
  3. Big files: detach() to a native uploader.
  4. Make every handler idempotent, and always send job.id as an idempotency key.
  5. Test with the app killed from the app switcher and with airplane mode toggled.