src/queue/worker.ts
BullMQ worker.
Developer guide
The connector lives in apps/baileys-connector. It owns Baileys sockets and consumes WhatsApp mutation jobs.
Key areas
src/Jobs come in through the worker, the manager drives the socket, and results go back to the API through the signed client. Session helpers sit underneath all of it.
src/queue/worker.ts
BullMQ worker.
src/whatsapp/manager.ts
Socket lifecycle.
src/whatsapp/messages.ts
WAMessage to API payload mapping.
src/internal/api-client.ts
HMAC-signed API client.
src/session
Crypto, S3, and session store helpers.
Rules
The connector is a disposable WhatsApp adapter. Keep it single, keep it thin, and keep its only way back to the API the signed callback path.
Streaming media encryption
Connector streaming uploads use a v2 AES-GCM envelope with the tag appended, so the connector can stream-encrypt and upload in constant memory instead of buffering the entire object.
Encrypt and upload as a stream, in constant memory.
Producing this layout forces full buffering of the object.
Never infer an object's encryption layout from its bytes. The rbx-env metadata is the record of the envelope, and copy operations must preserve it. Losing it causes auth-tag failures and terminal decryption errors.
Restarts
started_at in the heartbeatConnector restarts can overlap the old process and briefly find channel locks still held. Treating those channels as orphaned demoted them to disconnected and they never reconnected.
When a connect finds the channel lock held, the connector does a short in-place retry and then parks the channel, freeing the sweep slot instead of failing the connect. Parked channels probe the lock in the background every ~5s (plus jitter) and only re-run connect when the lock looks free.
connecting while parked
A parking channel must not emit connecting, because the old instance may still be delivering messages.
started_at
The connector includes its process start time (started_at, ISO-8601 Z) in the heartbeat snapshot so the API can apply a post-boot grace window.
New log lines report lock-waiting channels, when a channel acquires a lock after waiting, a boot reconciliation summary with lockHeldElsewhere, and per-heartbeat summaries of waiting channels.
Operator note. Connectors keep short retries to absorb immediate close-handler releases, but multi-minute handovers are handled by parking and background probes, so sweeps finish fast.
Networking
The per-address TCP connect-attempt timeout is set process-wide early in main(), so it takes effect before any outbound socket is opened. Nothing in the connector opens sockets at import time, so the top of main() is sufficient.
CONNECT_ATTEMPT_TIMEOUT_MSPer-address TCP connect-attempt timeout. Default 5000 ms; 0 uses Node's 250 ms default.
CONNECT_ATTEMPT_REPORT_MSSummary window. Default 60000 ms; 0 disables the monitor.
The monitor wraps net.Socket.prototype.connect listeners to count attempt timeouts, rescued sockets, and failed sockets. It classifies tallies by host class (whatsapp, media_cdn, object_storage, api, redis, other) and emits a single net: connect attempts timed out summary line per CONNECT_ATTEMPT_REPORT_MS window, only when there are timeouts.
The connector logs net: connect-attempt timeout at boot with {previousMs, effectiveMs, autoSelectFamily}, warns if automatic IPv4/IPv6 selection is off, and includes detail on manager: connection closed entries so failed reconnects show informative reasons.
Operational guidance. Perform a connector-only deploy and restart the fleet at a quiet hour. After the deploy, measure the reconnect success rate and the net: connect attempts timed out summaries to validate the improvement.
Load shedding
When mean event-loop lag stays high across consecutive 5s windows, the connector throttles bulk media downloads and avatar fetches to a controlled trickle rather than pausing the whole queue.
download_media jobs above priority 2 (bulk and prefetch sweeps, priority 6) and fetch_avatar. Jobs for send, send_media, send_album, and connect are never held back.
Interactive media is preserved: priority ≤ 2, which includes live ingest, continues to flow so agents still see new inbound photos promptly.
While shedding, a small number of bulk jobs (default one) remains in flight per worker, and a hard cap limits a single shedding episode.
Queue.pause()Admission uses rateLimit plus RateLimitError. This avoids Redis-persisted pauses that survive crashes and leaves no persistent pause keys.
off, dry_run, onSHED_MODE=off is the default. dry_run runs the state machine and logs would-shed and would-resume without changing anything, and on enforces. Unknown or inconsistent modes log a warning and fall back to off.
composing jobs older than 15s are completed locally without calling WhatsApp. paused is skipped only when this process previously skipped the latest composing for that chat and nothing sent by this process can still be showing. Default PRESENCE_STALE_MS=15000; 0 disables the rule.
No API change, and no change to priorities, queue names, or producer contracts. mark_read behaviour is unchanged and is never skipped. No migration is required.
History imports
Newly paired numbers are the only ones WhatsApp offers full history to, so concurrency is capped: MAX_CONCURRENT_IMPORTS bounds how many may import at once, and extra pairings queue rather than uploading faster.
Imports end when history has truly stopped: no batch for HISTORY_IMPORT_QUIET_MS and everything received has been uploaded. isLatest is not used to end an import. A hard ceiling, HISTORY_IMPORT_MAX_MS (default 60 minutes), prevents runaway imports; hitting it logs at error level and alerts Sentry.
Imports are keyed by channel, not socket. Transient socket reconnects do not finalize an in-progress import; only logout, an unrecoverable close, a manual disconnect, or channel eviction end it.
Batches are cut into upload chunks and posted in FIFO order. Chunks free memory as they finish, and decoded batch memory is released after its last chunk posts.
A 410 or 404 from the ingest API throws ChannelGoneError, cancels the import, and evicts the channel. 410 triggers a logout; 404 is confirmed via /exists first. Treat these as a stop signal rather than retrying.
importing and import_capacityThe connector reports the set of currently importing channels and its capacity (limit, admitting, heap stats), so the API can queue first-time pairings. The API stores import_seen_at to keep slots held, reconciles its queue against the connector report, admits and releases slots, and may return an evict list for deleted channels.
A V8-heap admission alarm pauses admitting new pairings above IMPORT_HEAP_PAUSE_PCT and resumes at IMPORT_HEAP_RESUME_PCT, which avoids memory and CPU freezes.
MAX_CONCURRENT_IMPORTSCap on concurrent history imports. Keep it ≤ MAX_CONCURRENT_SYNC_POSTS so no extra decoded history is held in memory.
HISTORY_IMPORT_QUIET_MSQuiet period before an import is considered finished.
HISTORY_IMPORT_MAX_MSHard ceiling on a single import.
IMPORT_HEAP_PAUSE_PCTHeap share at which admission pauses.
IMPORT_HEAP_RESUME_PCTHeap share at which admission resumes.
Do not work around the queue by listing or deleting channels/{id}/ wholesale. Session and media objects share prefixes, and unsafe deletes break imports. A history import is finished only after the connector reports no batches for a quiet period or hits the 60-minute ceiling; the connector must hold its slot until then.
Avatars
Profile-picture probes classify failures instead of treating every non-timeout error as a definitive “no picture”. This prevents transient transport errors from nulling avatars and putting contacts on a 7-day back-off.
Only 404 / item-not-found and 401 / not-authorized clear a stored photo. Timeouts remain deferred.
429 rate limits, socket and connection errors, 5xx, and unknown errors are recorded as deferred: the stored picture stays and the contact is retried later.
src/queue/avatar-error-class.tsWatch for the connector log line fetch_avatar: probe failed transiently; deferring….