FAQ and gotchas
Troubleshooting checklists, the questions people ask first, the mistakes people make first, and an honest look at what QueueKit is not.
Troubleshooting
Jobs are not running
Dump the queue first. It usually answers the question by itself:
Then walk this list in order:
- No handler. Was the module that calls
queue.define()imported at startup? Jobs of a type with no handler wait forever. In a background task, did you import your job modules there too? - Type name mismatch. The string passed to
define()must equal what was saved. If you renamed it, add an alias handler (Renaming a job type). - The queue is stopped.
autoStart: falseor astop()without a laterstart(). - Offline.
requiresNetworkdefaults to true. Is yournetworkmonitor reporting what you think? Without one, the queue assumes online. - Not due yet.
startsInMsabove is positive: adelayMsor a retry backoff. CheckattemptsandlastError. - A limit.
concurrency,limits[type].concurrencyor a rate window is full. Jobs wait for a slot. - Expired. A
ttlMselapsed: the job was dropped and anexpiredevent fired. - The app is suspended. JavaScript does not run in the background. See Background.
- Different storage. A different MMKV
idor queuenamethan before looks like an empty queue. - Expo Go. MMKV needs a development build.
Jobs run twice
-
By design: at-least-once. A job that was running when the app died runs
again. Make handlers idempotent and send
job.idas an idempotency key. -
Two queue instances (Fast Refresh). Editing the module that calls
createQueueduring development creates a second queue with the same name on the same storage. Both load the same jobs and both run them. Dispose the previous instance:
- Two queues, same name. Give each queue its own
name, or create them withlease: trueso only one runs jobs at a time (Several instances). - Removed a running job. The request may already have reached the server. Idempotency again.
Decorators do nothing or fail to compile
- Add
@babel/plugin-proposal-decoratorswith{ version: "2023-11" }to Babel, and restart Metro with--reset-cache. - Leave
experimentalDecoratorsoff intsconfig.json. - Construct the class: instance methods register when an instance is created.
- If it still fights you, use
queue.define(). It needs no setup.
The wrong handler runs for a job
Two define() (or register, or decorated methods) with the same type string: the last
one replaces the first, and every job of that type runs through it, with no error. Search your code for the
string, and keep strict on (it is the default outside development builds) so a duplicate throws
(Catching duplicate job names).
My payload came back different
Payloads are JSON. Dates become strings, undefined properties vanish, Maps and class
instances flatten. See Payload rules.
Large uploads keep timing out
The default per-attempt timeout is 30 seconds. Raise timeoutMs, split slow work into its
own queue, upload in chunks, or hand the transfer to a native uploader with
detach().
Storage keeps growing
Failed jobs are kept until you act. Surface them in a
sync issues screen, retry or discard them, and put
ttlMs on low-value jobs so they expire by themselves.
Common questions
Can I get a job's result?
No. A job has no result: the return value of a handler is ignored. Write the outcome where your app already looks (a database, a cache) from inside the handler, and read it there.
Can jobs depend on each other?
Chain them: have a handler enqueue the next step when it finishes (Chaining), or use the compress-then-upload pattern (Pipeline). Give steps deterministic ids so a restart cannot duplicate them.
Does it keep order?
Priority, then due time, then creation time. Not across failures: a retrying job lets later ones pass. Use a sequence number on the server for strict order (Chat recipe).
Is it encrypted?
Not by default. Payloads are stored as plain JSON in your MMKV instance. Use an MMKV instance with
an encryptionKey (Secrets and tokens), and do not put secrets
such as tokens in payloads. Enqueue with sensitive: true to keep a payload out of
event listeners and logs; it does not encrypt anything by itself.
How many jobs can it hold?
Hundreds to a few thousand is comfortable. Every job is loaded in memory at start-up, and the scheduler scans pending jobs when something changes. For tens of thousands of items, batch them into one job that carries a list or a reference to a local table.
Can I use it without React?
Yes. The core (@nijatk/react-native-queuekit) has no React dependency. Hooks live in @nijatk/react-native-queuekit/react.
Does it work on web or in tests?
The core is plain TypeScript, so it runs anywhere with a QueueStorage. On the web
use a localStorage adapter (Other backends). In tests use
memoryStorage().
Does the clock matter?
Delays, backoff and expiry use Date.now() with no protection against clock changes.
If the user sets the device time back, waiting jobs wait longer; forward, they fire or expire early.
Do not use ttlMs or delayMs for security-critical timing.
Can I schedule a job for 9am tomorrow?
With delayMs, yes, but it only runs if the app is alive then (or opens soon after). For exact
times use local notifications.
Is the API stable?
This is version 0.1. Expect additions, and possibly small changes, before 1.0. Pin the version.
Why MMKV and not SQLite or AsyncStorage?
| MMKV | SQLite | AsyncStorage | |
|---|---|---|---|
| Access | Synchronous | Asynchronous | Asynchronous |
Saved when enqueue() returns | Yes | Only if you await | Only if you await |
| Start-up | Instant, no schema | Open, migrate, query | Async load |
| Fit for queue state | Small, key-addressed records | Overkill; great for large relational data | Slow and not designed for hot writes |
| Encryption | Built in | Extra library | No |
A job queue reads and writes small records by key, very often, and needs each write to be durable before it moves on. That is exactly MMKV's shape. Keep large binary data on disk and queue its path.
How it compares
Not a scoreboard, just a guide to choosing. Details change, so check each project's own docs.
| Option | What it is | Choose it when |
|---|---|---|
| QueueKit | A persistent, network-aware job queue with retries, limits, hooks and native-task handoff. | You want one reliable outbox for uploads, forms and background work, independent of your data layer. |
| TanStack Query mutations | Cache and request library with paused, persistable mutations. | Your app is already built around it and your offline needs are mostly "replay my mutations". Pairs well with QueueKit (recipe). |
react-native-job-queue | A React Native job queue library. | You prefer its worker model. Compare maintenance activity and features first. |
redux-offline, react-native-offline | Redux-centred offline helpers and action queues. | You are all-in on Redux and want offline handled in reducers and middleware. |
expo-background-task, react-native-background-fetch | OS background wake-ups. | Complementary: use them to give QueueKit time to drain(). |
Native uploaders (react-native-background-upload and similar) | OS-level transfer services. | Complementary: for big files, detach() to them and let QueueKit track the job. |
What QueueKit is not
- Not a server queue. It does not talk to Redis, SQS or a broker. It lives on the device.
- Not a scheduler. It has delays, not cron, and cannot wake a killed app by itself.
- Not a sync engine. No conflict resolution, no cross-device state. It reliably delivers your requests; what they mean is up to you.
- Not a guarantee of exactly-once. At-least-once, with the tools (ids, idempotency keys) to make repeats harmless.
- Not a replacement for native background transfers. It integrates with them.