Storage, network and testing

The two pluggable parts of the queue, how payload upgrades work, how to run several queues, and how to test everything.

Storage

The queue persists jobs through a tiny synchronous interface:

A react-native-mmkv instance already matches it, so you pass it directly. Synchronous is the important word: it lets enqueue() guarantee the job is saved before it returns, and lets the queue load everything at creation with no async start-up.

MMKV options

Sharing an instance with other app data works too: the queue only reads keys that start with its own prefix and ignores everything else.

What is stored

Each job is one key, queuekit:<queue name>:job:<id>, holding the job as JSON. Updating a job rewrites only that key.

Secrets and tokens

Everything in a payload, tokens included, is saved as plain text in the storage you pass in. It stays there while the job waits, retries or is offline, and after a restart. A completed job is deleted; a failed one keeps its payload until you retry or remove it, or retention cleans it up.

The best fix is to keep the secret out of the payload and read it when the handler runs. The job then holds nothing sensitive, and a token that expired while the job waited is never reused.

If a payload really must hold something private:

ToolWhat it doesWhat it does not do
MMKV encryptionKeyEncrypts the data on disk.Protect the payload in memory, or from your own event listeners.
sensitive: trueHides the payload and handler result from on() and observe(), so it cannot reach logs or analytics.Encrypt anything. It is still stored as plain JSON unless the storage is encrypted.
Development warningLogs once when a payload key contains token, password, secret or authorization.Run in production builds.

Use both together, and keep the encryption key in the Keychain or Keystore, not in source code. queue.getJobs() and the job returned by enqueue() still show the payload, and error messages in lastError are not redacted.

Other backends

AsyncStorage will not work

Its API is asynchronous. A queue on top of it could not promise that a job is saved when enqueue() returns, so QueueKit does not support it. Any store with the four synchronous methods above does.

Network monitors

Check that your server is really reachable

Wi-Fi without internet (hotels, captive portals) reports as connected. If that matters, probe your own API:

Require several signals at once

A manual switch for QA

Migrations

Jobs outlive app updates. If you change a payload's shape, jobs saved by the previous version still carry the old one. A migration upgrades them once, when the queue loads.

SituationResult
Saved job has no versionTreated as version 1.
Saved version is lower than versionmigrate(payload, savedVersion) runs once. The result and the new version are saved.
Saved version is the same or newerLeft untouched.
migrate throwsThat job becomes failed with lastError "Migration failed: ...". Other jobs are unaffected.
A type with no migrationUntouched.
New jobsStamped with the current version.

Renaming a job type

Migrations change payloads, not names. If you rename a type, jobs saved under the old name have no handler and would wait forever. Keep an alias handler for the old name:

Multiple queues

Each createQueue call is an independent queue with its own concurrency, timeout, retry policy and limits. Queues are separated in storage by name, so they can safely share one MMKV instance.

When to split: different timeouts, different failure tolerance, or you do not want bulk work to share concurrency slots with user-facing work. For simple prioritization, one queue with priority is enough.

Hooks with several queues

QueueProvider holds one queue. For others, nest providers (the inner one wins for its subtree), or use useSyncExternalStore(analytics.subscribe, analytics.getStats) directly.

Several instances sharing one storage

Normally an app has exactly one JavaScript runtime and one queue, and none of this applies. It matters when two instances can exist over the same storage and name: a second JS context in the same process, an app extension, or a development reload that left the old queue running. Each instance would load the same jobs and run them, so they run twice.

The lease option lets only one instance, the leader, run jobs at a time.

SituationWhat happens
First instance startsIt takes the lease and leads. Later instances become followers.
A follower calls enqueue()The job is saved. The leader finds it on its next heartbeat (every ttlMs / 3) and runs it. Until then the leader does not know about it, so orderKey order across instances is best effort.
The leader stops while idleIt releases the lease. A follower takes over on its next heartbeat, or immediately on flush().
The leader stops with a job still runningIt keeps renewing the lease until that job ends, then releases it, so the job is never run twice.
The leader diesIts lease expires after ttlMs. A follower takes over, treats the jobs the dead leader was running as interrupted, and runs them again.
drain() on a followerResolves true at once: this instance has nothing to run.
Followers' view of the jobsRefreshed from storage every ttlMs / 3, so hooks stay roughly current.
Limits

Signing out and switching users

Jobs often belong to one account. Pick a strategy:

Do not leave jobs from user A runnable under user B's session: handlers read the current token when they run, so they would act as the wrong user.

Testing

Everything is plain objects and promises, so tests need no mocks of the queue itself. Use memoryStorage(), a fake network, and await queue.drain().

Offline behaviour

Persistence and restarts

Tips

TypeScript tips