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

LayerWhat to do
ClientSend job.id (or a business id you pass as id) as Idempotency-Key.
ServerRemember the key and its response for a day or more. A repeat returns the stored result instead of doing the work again.
ConflictsIf 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 supportUse 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.

Why the flush job holds no events

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.