Troubleshooting & FAQ
Find the symptom, read the cause, apply the fix. This page covers the failures operators hit most — container, session connect, QR timeout, disconnects, send failures, webhooks, auth, media, memory, and SQLite locks — then lists every HTTP status code and application error code the API returns.
For first-time setup, see Installation and Configuration. Terms used here are defined in the Glossary.
Quick diagnostics
Run these first — they isolate which layer is failing (process, database, engine, or a single session).
# 1. Is the API process up? (no auth required)
curl http://localhost:2785/api/health
# 2. Are the database and adapters ready? (no auth required)
curl http://localhost:2785/api/health/ready
# 3. Inspect one session's status
curl -H "X-API-Key: YOUR_API_KEY" \
http://localhost:2785/api/sessions/SESSION_ID
# 4. Container status and recent logs
docker compose ps
docker compose logs openwa --tail=100
YOUR_API_KEY is the key you set in your environment; see
Authentication for how it is issued. SESSION_ID is the
session id returned when you create a session — a generated UUID, not the name
you chose. Lookups (and the auth-folder paths below) use this UUID, so paste the
id from the create response, not your session name.
A healthy session returns "status": "ready". Any other value points you at a
section below.
Session states
Most connection problems are a session stuck in a non-ready state. A session
moves through these seven values (lowercase, as returned by the API):
| Status | Meaning | What to do |
|---|---|---|
created | The session record exists but the engine has not started | Start the session |
initializing | The engine is starting up | Wait; if it never advances, check logs |
qr_ready | A QR code is waiting to be scanned | Fetch and scan the QR within ~60 s |
authenticating | QR scanned, linking the device | Wait; if stuck, see the section below |
ready | Connected and able to send/receive | None — this is the goal state |
disconnected | The link dropped | Reconnect; check the phone's linked devices |
failed | A terminal engine error occurred | Read lastError, fix, recreate |
When status is failed, the session response includes a human-readable
lastError. See Sessions for the full lifecycle.
Connection issues
Container won't start
Problem: docker compose up fails, the container exits immediately, or you see
"port already in use."
Cause: Another process is bound to port 2785, or a previous run left a broken
container or volume state.
Fix:
# Find and free whatever holds the port
lsof -i :2785
kill -9 $(lsof -t -i:2785)
# Read why the container exited
docker compose logs openwa
# Pull the current image and restart cleanly
docker compose pull
docker compose up -d
If it still exits, run docker compose logs openwa and match the first error to a
section below.
Session won't connect (stuck at initializing or qr_ready)
Problem: A QR code is generated but the session never reaches ready, or it
falls back to disconnected after you scan.
Cause: An expired QR, a corrupted auth folder, a browser crash, or an outbound network/firewall block.
Fix: Match the cause, then re-scan.
| Cause | Fix |
|---|---|
| Expired QR | Fetch a fresh one — a QR is valid for about 60 seconds |
| Corrupted auth folder | Delete the session's auth data and re-scan |
| Browser crash (whatsapp-web.js) | Restart the container |
| Network / firewall block | Verify outbound connectivity and any proxy |
# whatsapp-web.js engine (default): auth lives under SESSION_DATA_PATH
# (default ./data/sessions), in a session-<id> subfolder
rm -rf ./data/sessions/session-SESSION_ID
# Baileys engine: auth lives under BAILEYS_AUTH_DIR (default ./data/baileys)
rm -rf ./data/baileys/SESSION_ID
docker compose restart openwa
Deleting an auth folder unlinks that session from WhatsApp. You must scan a new QR to reconnect.
Session stuck at authenticating, never reaches ready
This affects the whatsapp-web.js engine only. With ENGINE_TYPE=baileys, skip
this section.
Problem: After scanning, the phone links the device but the session stays at
authenticating indefinitely. Common on ARM64 hosts (for example a Raspberry Pi).
Cause: whatsapp-web.js auto-selects a WhatsApp Web client version, and an incompatible version stalls the post-link sync.
Fix: Pin a known-good WhatsApp Web version, then restart the container:
WWEBJS_WEB_VERSION=2.3000.1040641150-alpha
Browse available versions in the html/ folder of
wppconnect-team/wa-version. Set the
value to latest, auto, or off (or leave it unset) to restore auto-selection.
QR generation times out on slow first boot (WSL2 / low-resource)
whatsapp-web.js only.
Problem: On the first launch, no QR appears and the session fails after about 30 seconds — often inside WSL2 or a resource-constrained container.
Cause: whatsapp-web.js waits a fixed 30000 ms for WhatsApp Web to finish loading before it generates the QR. A slow first boot can exceed that window.
Fix: Raise the boot/inject wait (in milliseconds) and restart:
WWEBJS_AUTH_TIMEOUT_MS=120000 # allow up to 2 minutes
Leave it unset to keep the 30000 ms default.
Session fails to launch — chrome_crashpad_handler: --database is required
whatsapp-web.js (Chromium/Puppeteer) only.
Problem: The session never starts. The log shows Failed to launch the browser process with chrome_crashpad_handler: --database is required. Seen on hardened,
read_only containers.
Cause: Chromium resolves its home directory from the system passwd entry and
ignores $HOME. The non-root runtime user has no home directory, so on a
read-only rootfs Chromium aborts at launch.
Fix: Give Chromium writable config and cache directories. The bundled image and
docker-compose.yml already do this on a tmpfs /tmp. For a custom container, set
both paths to a writable, existing location and mount a writable /tmp:
XDG_CONFIG_HOME=/tmp/.config
XDG_CACHE_HOME=/tmp/.cache
# Pre-create both as the runtime user, and mount a writable tmpfs at /tmp:
# compose: tmpfs: ["/tmp"]
# k8s: an emptyDir volume mounted at /tmp
Do not work around this by removing --no-sandbox hardening or using
seccomp:unconfined. It does not help and it widens the attack surface.
Frequent disconnections
Problem: A session drops to disconnected every few hours and needs frequent
re-scans.
Cause: A logout from the phone's linked devices, memory pressure, an unstable network, or a blocked IP address.
Fix:
- Open WhatsApp on the phone → Linked devices and confirm the device is still linked.
- Give the container more RAM (see High memory usage).
- Check outbound connectivity from the host.
- If your hosting IP is being blocked by WhatsApp, route through a residential proxy.
Messaging issues
Messages not sending
Problem: The send call returns 2xx but the message isn't delivered, or it
returns an error.
Cause: Most often a bad recipient format, a disconnected session, rate limiting, or oversized media.
Fix: Match the status code:
| Cause | Status | Fix |
|---|---|---|
| Invalid recipient format | 400 | Use the JID format 628123456789@c.us |
| Session not connected | 409 | Wait for ready or reconnect the session |
| Rate limited | 429 | Slow your send rate (see Rate limiting) |
| Media too large | 413 | Reduce the file or raise the media cap |
| Number not on WhatsApp | 2xx, no delivery | Verify the number first |
Verify a number is on WhatsApp before sending:
curl -H "X-API-Key: YOUR_API_KEY" \
"http://localhost:2785/api/sessions/SESSION_ID/contacts/check/628123456789"
{
"number": "628123456789",
"exists": true,
"whatsappId": "628123456789@c.us"
}
Recipient JIDs follow a fixed shape:
| Recipient | JID format | Example |
|---|---|---|
| Individual | <number>@c.us | 628123456789@c.us |
| Group | <groupId>@g.us | 120363123456789@g.us |
See Sending messages for the full message API and Groups for group sends.
Webhook not firing
Problem: Messages arrive in WhatsApp but your webhook endpoint is never called.
Cause: No webhook is configured for the session, the URL is unreachable from the
container, or your endpoint returns a non-2xx and exhausts the retries.
Fix:
# 1. Confirm a webhook is registered for the session — note its id
curl -H "X-API-Key: YOUR_API_KEY" \
http://localhost:2785/api/sessions/SESSION_ID/webhooks
# 2. Force a delivery with the built-in test endpoint (WEBHOOK_ID from step 1).
# It POSTs a test payload to your URL and returns the delivery result.
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
http://localhost:2785/api/sessions/SESSION_ID/webhooks/WEBHOOK_ID/test
# 3. There is no delivery-history log — read the server logs for the attempt
docker compose logs openwa --tail=200 | grep -i webhook
# 4. Confirm your endpoint accepts an unauthenticated POST quickly
curl -X POST https://your-app.example.com/webhook \
-H "Content-Type: application/json" \
-d '{"test": true}'
Your endpoint must return a 2xx quickly. OpenWA retries failed deliveries with a
fixed-delay backoff, controlled by these environment variables:
| Variable | Default | Controls |
|---|---|---|
WEBHOOK_TIMEOUT | 10000 | Per-attempt timeout, in milliseconds |
WEBHOOK_MAX_RETRIES | 3 | Retry attempts after the first failure |
WEBHOOK_RETRY_DELAY | 5000 | Delay between attempts, in milliseconds |
If the container can reach the public internet but not your endpoint, the URL is
probably resolving to a host-only address. Use host.docker.internal (Docker
Desktop) or the service name on a shared Docker network rather than localhost.
See Webhooks for the event list and payload shapes.
Media upload fails
Problem: A send returns 413 Payload Too Large, or media won't upload.
Cause: OpenWA enforces two separate limits. A decoded media blob may not exceed
MEDIA_DOWNLOAD_MAX_BYTES (default 50 MiB), and the whole HTTP request body is
bounded by BODY_SIZE_LIMIT. A base64 payload counts against both.
Fix:
- Prefer sending media by URL for large files — the engine downloads it server-side instead of inflating your request body.
- If you must send large base64 media, raise both limits and restart:
MEDIA_DOWNLOAD_MAX_BYTES=104857600 # 100 MiB media cap
BODY_SIZE_LIMIT=120mb # whole-request body limit
A request that exceeds the media cap returns 413 Payload Too Large with the
standard error envelope — a statusCode, a message, and an error string. There
is no code field on this response; branch on the status, not on a payload code.
Authentication issues
Every request returns 401
Problem: All authenticated endpoints return 401 Unauthorized, even ones that
worked before.
Cause: The X-API-Key header is missing, misspelled, or carries the wrong key.
Fix: Send the header on every request to an authenticated route. The header name is exact and case-sensitive in its documented form:
# Wrong — no key, returns 401
curl http://localhost:2785/api/sessions
# Right
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:2785/api/sessions
The health endpoints (/api/health, /api/health/live, /api/health/ready) are
unauthenticated by design — use them to confirm the server is up without a key. See
Authentication for how keys are issued and rotated.
Performance issues
High memory usage
Problem: The container uses a large amount of RAM, or the host OOM-kills it.
Cause: With the default whatsapp-web.js engine, each session runs its own Chromium instance (roughly 300–500 MB RAM). Total memory scales with session count.
Fix: Cap memory, trim Chromium flags, or switch to the lighter engine.
services:
openwa:
deploy:
resources:
limits:
memory: 2G
environment:
# whatsapp-web.js engine only
- PUPPETEER_ARGS=--disable-dev-shm-usage,--disable-gpu,--no-sandbox
Switching to ENGINE_TYPE=baileys removes Chromium entirely and dramatically lowers
per-session memory. See Scaling for capacity planning.
Database locked (SQLite)
Problem: Writes fail intermittently with SQLITE_BUSY or "database is locked,"
and response times spike.
Cause: SQLite serializes writes. Under concurrent sessions or a write-heavy workload, writers contend for the single database lock.
Fix: Ensure write-ahead logging is on, then plan a move to PostgreSQL.
# Data DB defaults to ./data/openwa.sqlite (auth/audit DB is ./data/main.sqlite)
sqlite3 ./data/openwa.sqlite "PRAGMA journal_mode;" # expect: wal
sqlite3 ./data/openwa.sqlite "PRAGMA journal_mode=WAL;"
Past roughly 5 concurrent sessions, or for any write-heavy deployment, switch the
Database adapter to PostgreSQL. See the Migration guide.
Docker issues
Volume permission denied
Problem: "Permission denied" when writing to the data directory; auth files don't persist across restarts.
Cause: The host directory is owned by a different user than the container's runtime user.
Fix:
sudo chown -R $(id -u):$(id -g) ./data
docker compose restart openwa
Podman: Docker socket missing or container stays unhealthy
Problem: On Podman you see FileNotFoundError for the Docker socket, or the
container starts but stays unhealthy.
Cause: Podman's rootless socket is inactive by default, and a few Docker
conventions (unqualified image names, node -e healthchecks) behave differently
under Podman.
Fix: Start the Podman socket and export DOCKER_HOST:
systemctl --user start podman.socket
systemctl --user enable podman.socket
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock
When building a custom image under Podman, use fully-qualified image names (for
example docker.io/node:22-slim) and a curl-based healthcheck rather than
node -e.
Rate limiting
OpenWA applies a single global rate limit across all clients (keyed on the resolved
client IP), enforced in three tiers. Exceeding any tier returns 429 Too Many Requests.
| Tier | Default limit | Window |
|---|---|---|
| Short | 10 requests | 1 second |
| Medium | 100 requests | 60 seconds |
| Long | 1000 requests | 1 hour |
There is no per-session rate-limit endpoint. To raise the limits, set the
corresponding configuration values and restart. On a 429, back off and retry.
Error code reference
HTTP status codes
The API uses the standard NestJS error envelope. Every error response is JSON of the shape:
{
"statusCode": 404,
"message": "Session \"SESSION_ID\" not found",
"error": "Not Found"
}
| Code | Meaning | Common cause | Fix |
|---|---|---|---|
400 | Bad Request | Invalid body, params, or JID format | Check the request payload |
401 | Unauthorized | Missing or invalid API key | Send a valid X-API-Key header |
404 | Not Found | Unknown session, message, or route | Verify the id exists |
409 | Conflict | Session id already exists, or session not connected | Use a new id, or wait for ready |
413 | Payload Too Large | Media or body exceeds the cap | Reduce size or raise the limit |
429 | Too Many Requests | Rate limit exceeded | Back off and retry |
500 | Internal Server Error | Unhandled server error | Check the logs and report it |
501 | Not Implemented | Operation unsupported by the active engine | Use a supported engine or operation |
503 | Service Unavailable | Dependency or session temporarily down | Retry after the dependency recovers |
Bulk-send result codes
These codes are not HTTP statuses. They appear only on per-message results
inside a bulk batch, retrieved from
GET /api/sessions/{sessionId}/messages/batch/{batchId}. Each failed entry carries
a { code, message } object so you can branch on why one message in the batch
failed. An ordinary POST .../messages send does not return these codes — it
returns a normal HTTP status (see the table above).
| Code | Meaning | Fix |
|---|---|---|
SEND_BLOCKED | Destination address was refused (SSRF/allow-list block) | Check the recipient and any allow/deny list |
SEND_FAILED | The engine rejected the send | Read the accompanying message for the cause |
Engine-not-ready errors surface as 409 Conflict ("Session is not connected"), and
referencing a message outside the engine's lookup window surfaces as 404 Not Found. Both follow the standard HTTP envelope above.
Frequently asked questions
How many sessions can I run on one instance?
It depends on host resources and the engine. With the default whatsapp-web.js engine
(~300–500 MB RAM per session): roughly 3–5 sessions on 2 GB, 8–10 on 4 GB, 15–20 on
8 GB. ENGINE_TYPE=baileys is browser-free and fits many more on the same hardware.
See Scaling.
How do I keep my number from getting banned? OpenWA uses the unofficial WhatsApp Web protocol, so there is inherent risk. Use a dedicated number (not your personal one), avoid bulk or unsolicited messaging, ramp volume gradually, vary message content, add delays between sends, and don't message people who haven't contacted you first.
Can I use a WhatsApp Business account? Yes — both personal and WhatsApp Business app accounts work. The official WhatsApp Business API (Meta Cloud API) is a different product and is not what OpenWA uses.
How do I send to a group?
Use the group JID (...@g.us) as the chatId. See Groups.
How do I run behind a reverse proxy? Forward WebSocket upgrades and set generous idle timeouts — OpenWA keeps a long-lived connection per dashboard client on the same port as the REST API. See Deployment.
How do I integrate with n8n? See the n8n integration guide.
What webhook events can I subscribe to?
Message events (message.received, message.sent, message.ack,
message.failed, message.revoked, message.reaction) and session events
(session.status, session.qr, session.authenticated, session.disconnected).
Group events are reserved but not currently emitted. See
Webhooks.
Getting help
Before opening an issue:
- Re-check this page for your symptom.
- Read the logs:
docker compose logs openwa --tail=200. - Try a restart and, if relevant, a clean re-scan.
- Search existing issues — someone may have hit it already.
When you report, include your OpenWA version (v0.7.6), deployment (Docker/OS),
engine (ENGINE_TYPE), Database adapter, session count, reproduction steps, and
sanitized logs.
Next steps
- API conventions — auth, pagination, and the error envelope in detail
- Full API reference — every endpoint, field, and status code
- Glossary — definitions for JID, session states, adapters, and more