Recipes: APIs and networking
Patterns for talking to servers from jobs: one shared request helper, authentication, rate limits, GraphQL, batching, cache integration and protecting a struggling backend.
One shared request helper
Put the error classification in one place, so every job behaves the same. It sends an
idempotency key, honors Retry-After, and decides which failures are worth
retrying.
Every job then reads like this:
Authentication and token refresh
Never put tokens in payloads: a job may run hours later. Read the token when the handler runs, refresh once on 401, and stop the queue if the session is truly gone.
Respecting a rate-limited API
Prevent 429s instead of reacting to them with a rate limit, and still handle them with
Retry-After when they happen.
Jobs held back by a limit wait in pending and are woken by a timer. Other job
types keep flowing. The request helper above converts a real 429 into
RetryAfterError, so the delay the server chose is the delay you use.
Idempotency, end to end
| Layer | What to do |
|---|---|
| Client | Send job.id (or a business id you pass as id) as Idempotency-Key. |
| Server | Remember the key and its response for a day or more. A repeat returns the stored result instead of doing the work again. |
| Conflicts | If a repeat arrives while the first is still processing, return 409 and let the client retry later. Treat "already exists with this key" as success in the handler. |
| No server support | Use PUT or upserts keyed by a client-generated id so a repeat overwrites with the same data. |
A generic GraphQL mutation job
GraphQL returns HTTP 200 with an errors array, so classify by error code, not by status.
Analytics: fire and forget, batched
Simple: one job per event
Batched: fewer requests
Buffer events in storage and send them together shortly after the last one.
Its payload is empty and it reads the buffer when it runs. That is why
dedupeKey is safe here: a request absorbed by an already-pending flush will be
picked up by it, and events that arrive during a send are handled by the follow-up flush.
Chaining: push, then pull
A job can enqueue the next step. Put the id of the chain in each step so a restart cannot duplicate it.
Alternatively, keep the steps independent and chain from outside with
queue.on("completed", ...). Inside the handler is simpler because it is saved
with the push; if the app dies between them, the push runs again and enqueues the pull.
TanStack Query (React Query) integration
QueueKit sends the mutation reliably; Query keeps your UI cache fresh. They complement each other.
Circuit breaker
If the backend is struggling, hammering it with retries from a million devices makes things worse. Pause the queue after a run of failures.
Combined with exponential backoff and jitter, this keeps load predictable during an outage. Make the pause longer on repeated trips if you like.
Priority lanes
Priority decides which eligible job starts next. If bulk work must never compete for the
same slots, give it its own queue with its own concurrency
(Multiple queues).
Detect "connected but no internet"
Captive portals and dead Wi-Fi look online to the operating system. Add a reachability monitor that probes your API: Network monitors.