I was running 50+ WhatsApp sessions on a single Node.js server.
Memory usage? 8GB. CPU? Constantly spiking. And every few hours, something would crash.
So I rewrote the whole thing in Rust. Now it handles 200+ sessions on 512MB RAM.
Here's the whole story.
The Problem With Every WhatsApp API Out There
Every WhatsApp gateway I found had the same issues:
- Baileys + Node.js — Works great until you scale. Then memory leaks eat you alive.
- Python solutions — Slow. Really slow.
- Paid APIs — $50/month per number? Fuck that.
I needed something that could:
- Handle hundreds of sessions simultaneously
- Not crash every 6 hours
- Actually be fast
- Run on a $5 VPS
Nothing fit. So I built it.
Why Rust?
I know, I know. "Rust is hard." "Just use Go."
But here's the thing — Rust's memory safety isn't just a flex. When you're managing hundreds of WebSocket connections and encryption keys, you need guarantees.
Plus, the whatsapp-rust library exists. Someone already did the hard part of reverse-engineering the WhatsApp Web protocol. I just needed to wrap it in an API.
What I Built: WA-RS
WA-RS is a multi-session WhatsApp REST API gateway. One server, unlimited WhatsApp accounts.
The stack:
| Component | Tech |
|---|---|
| Runtime | Rust (Nightly) |
| Web Framework | Axum 0.8 |
| Database | PostgreSQL |
| Templates | Askama |
| API Docs | OpenAPI 3.0 / Swagger |
Features That Actually Matter
✓ Multi-session — Run 100+ WhatsApp accounts on one server
✓ QR Code & Pair Code — Two ways to link devices
✓ Rich Messages — Text, images, video, audio, docs, stickers, location
✓ Webhooks — Real-time events with HMAC-SHA256 signatures
✓ Web Dashboard — Visual management, no CLI needed
✓ Swagger UI — Test endpoints without Postman
Getting Started (It's Stupid Simple)
Option 1: Docker (Recommended)
Pull from Docker Hub:
docker pull fdciabdul/wa-rs:latest
Option 2: Docker Compose (Full Stack)
Create a docker-compose.yml:
services:
wa-rs:
image: fdciabdul/wa-rs:latest
ports:
- "3451:3451"
environment:
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=wagateway
- JWT_SECRET=change-this-in-production
volumes:
- wa_sessions:/app/whatsapp_sessions
depends_on:
- postgres
postgres:
image: postgres:16-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=wagateway
volumes:
- pg_data:/var/lib/postgresql/data
volumes:
wa_sessions:
pg_data:
Run it:
docker compose up -d
That's it. Server running on http://localhost:3451.
Option 3: Build From Source
For the masochists who want to compile themselves:
# Clone the repo
git clone https://github.com/fdciabdul/wa-rs.git
cd wa-rs
# Install Rust nightly (required)
rustup default nightly
# Set up your .env
cp .env.example .env
# Edit .env with your Postgres credentials
# Build and run
cargo run --release
The API
All endpoints require authentication. Get your token first (see Authentication section below).
Create a Session
curl -X POST http://localhost:3451/api/v1/sessions \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "my-session",
"name": "My WhatsApp",
"webhook": {
"url": "https://your-server.com/webhook",
"secret": "webhook-secret",
"events": ["message", "connected", "disconnected"]
}
}'
Connect a Session
curl -X POST http://localhost:3451/api/v1/sessions/my-session/connect \
-H "Authorization: Bearer YOUR_TOKEN"
Get QR Code
curl http://localhost:3451/api/v1/sessions/my-session/qr \
-H "Authorization: Bearer YOUR_TOKEN"
Returns base64 PNG. Scan it with WhatsApp. Done.
Get Pair Code (Alternative to QR)
curl -X POST http://localhost:3451/api/v1/sessions/my-session/pair-code \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"phone": "628123456789"}'
Returns an 8-digit code. Enter it in WhatsApp → Settings → Linked Devices → Link with phone number.
Send a Text Message
curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/text \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "628123456789",
"text": "Hello from WA-RS!"
}'
Send an Image
curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/image \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "628123456789",
"url": "https://example.com/image.jpg",
"caption": "Check this out!"
}'
Send a Video
curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/video \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "628123456789",
"url": "https://example.com/video.mp4",
"caption": "Watch this"
}'
Send Audio
curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/audio \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "628123456789",
"url": "https://example.com/audio.mp3",
"ptt": true
}'
Set ptt: true for voice note style, false for regular audio file.
Send a Document
curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/document \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "628123456789",
"url": "https://example.com/document.pdf",
"filename": "invoice.pdf",
"caption": "Here is the invoice"
}'
Send a Sticker
curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/sticker \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "628123456789",
"url": "https://example.com/sticker.webp"
}'
Send Location
curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/location \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "628123456789",
"latitude": -6.2088,
"longitude": 106.8456,
"name": "Jakarta",
"address": "Jakarta, Indonesia"
}'
Send Contact Card
curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/contact \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "628123456789",
"contact": {
"display_name": "John Doe",
"phones": [
{"number": "+1234567890", "phone_type": "CELL"}
]
}
}'
Edit a Message
curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/edit \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "628123456789",
"message_id": "3EB0ABC123...",
"text": "Updated message text"
}'
React to a Message
curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/react \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "628123456789",
"message_id": "3EB0ABC123...",
"emoji": "👍"
}'
Disconnect a Session
curl -X POST http://localhost:3451/api/v1/sessions/my-session/disconnect \
-H "Authorization: Bearer YOUR_TOKEN"
Delete a Session
curl -X DELETE http://localhost:3451/api/v1/sessions/my-session \
-H "Authorization: Bearer YOUR_TOKEN"
The Dashboard
I hate CLIs for management. So I built a web dashboard.
Go to http://localhost:3451/dashboard and you get:
- Session overview — See all accounts, connection status at a glance
- Create sessions — Point and click, no curl commands needed
- QR codes — Scan right from the browser
- Pair codes — Link with phone number instead of QR
- Webhook config — Set up integrations visually
- Settings — View your API token, endpoints, version info
The design is dark mode only. Because light mode is a war crime.
Dashboard Routes
All public, no auth needed:
| Route | What it shows |
|---|---|
/dashboard |
Main overview with stats |
/dashboard/sessions |
List all sessions |
/dashboard/sessions/new |
Create new session form |
/dashboard/sessions/:id |
Session detail, QR code, actions |
/dashboard/settings |
API token, endpoints, version |
Authentication
Getting Your Token
On first startup, the server generates a superadmin token. Two ways to find it:
Option 1: Check the logs
docker compose logs wa-rs | grep "SUPERADMIN"
You'll see something like:
┌─────────────────────────────────────────────────────────────┐
│ SUPERADMIN TOKEN │
├─────────────────────────────────────────────────────────────┤
│ eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzdXBlcmFkbWluIiwicm9sZSI... │
├─────────────────────────────────────────────────────────────┤
│ Tip: Set SUPERADMIN_TOKEN in .env to use a fixed token │
└─────────────────────────────────────────────────────────────┘
Option 2: Dashboard
Go to http://localhost:3451/dashboard/settings — the token is right there.
Using the Token
Include it in every API request:
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..." \
http://localhost:3451/api/v1/sessions
Using Swagger UI
- Go to
http://localhost:3451/swagger-ui - Click the Authorize button (top right)
- Paste your token
- Now all requests include the token automatically
Fixed Token (Production)
Don't want a random token every restart? Set it in your environment:
SUPERADMIN_TOKEN=your-fixed-token-here
Webhooks
Real-time events. When someone messages your WhatsApp, your server knows instantly.
Supported Events
| Event | When it fires |
|---|---|
message |
Incoming message received |
connected |
Session connected to WhatsApp |
disconnected |
Session disconnected |
receipt |
Message delivered/read |
presence |
Contact online/offline status |
qr_code |
New QR code generated |
Webhook Payload Example
{
"event": "message",
"session_id": "my-session",
"timestamp": 1704067200,
"data": {
"from": "628123456789@s.whatsapp.net",
"message_id": "3EB0ABC123...",
"type": "text",
"body": "Hello!"
}
}
Signature Verification
Every webhook includes an X-Signature header — HMAC-SHA256 of the payload using your secret.
import hmac
import hashlib
def verify_signature(payload, signature, secret):
expected = hmac.new(
secret.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Verify it. Don't trust unverified webhooks.
Register Webhook via API
curl -X POST http://localhost:3451/api/v1/sessions/my-session/webhooks \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhook",
"secret": "your-webhook-secret",
"events": ["message", "connected"]
}'
Contacts & Groups
Check if Numbers are on WhatsApp
curl -X POST http://localhost:3451/api/v1/sessions/my-session/contacts/check \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"phones": ["628123456789", "628987654321"]
}'
Get Contact Info
curl -X POST http://localhost:3451/api/v1/sessions/my-session/contacts/info \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"phones": ["628123456789"]
}'
Get Profile Picture
curl http://localhost:3451/api/v1/sessions/my-session/contacts/628123456789@s.whatsapp.net/picture \
-H "Authorization: Bearer YOUR_TOKEN"
List Groups
curl http://localhost:3451/api/v1/sessions/my-session/groups \
-H "Authorization: Bearer YOUR_TOKEN"
Get Group Info
curl http://localhost:3451/api/v1/sessions/my-session/groups/123456789@g.us/info \
-H "Authorization: Bearer YOUR_TOKEN"
Presence & Chat State
Set Online/Offline Status
curl -X POST http://localhost:3451/api/v1/sessions/my-session/presence/set \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"status": "available"
}'
Options: available, unavailable
Send Typing Indicator
curl -X POST http://localhost:3451/api/v1/sessions/my-session/chatstate/typing \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "628123456789@s.whatsapp.net"
}'
Send Chat State
curl -X POST http://localhost:3451/api/v1/sessions/my-session/chatstate/send \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "628123456789@s.whatsapp.net",
"state": "composing"
}'
Options: composing, recording, paused
Blocking
Get Block List
curl http://localhost:3451/api/v1/sessions/my-session/blocking/list \
-H "Authorization: Bearer YOUR_TOKEN"
Block a Contact
curl -X POST http://localhost:3451/api/v1/sessions/my-session/blocking/block \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jid": "628123456789@s.whatsapp.net"
}'
Unblock a Contact
curl -X POST http://localhost:3451/api/v1/sessions/my-session/blocking/unblock \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jid": "628123456789@s.whatsapp.net"
}'
Check if Blocked
curl http://localhost:3451/api/v1/sessions/my-session/blocking/check/628123456789@s.whatsapp.net \
-H "Authorization: Bearer YOUR_TOKEN"
Performance
Real numbers from production:
| Metric | Node.js (Baileys) | Rust (WA-RS) |
|---|---|---|
| Memory (50 sessions) | ~4GB | ~200MB |
| Memory (200 sessions) | Crashes | ~800MB |
| Startup time | 3-5 seconds | <1 second |
| Message latency | 100-300ms | 20-50ms |
| CPU usage (idle) | 5-15% | <1% |
Rust isn't just faster. It's a different league.
Full API Reference
Every endpoint at a glance:
Sessions
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/sessions |
Create session |
GET |
/api/v1/sessions |
List all sessions |
GET |
/api/v1/sessions/:id |
Get session info |
DELETE |
/api/v1/sessions/:id |
Delete session |
GET |
/api/v1/sessions/:id/status |
Get session status |
POST |
/api/v1/sessions/:id/connect |
Start connection |
POST |
/api/v1/sessions/:id/disconnect |
Disconnect |
GET |
/api/v1/sessions/:id/qr |
Get QR code |
POST |
/api/v1/sessions/:id/pair |
Get pair code |
GET |
/api/v1/sessions/:id/device |
Get device info |
Messages
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/sessions/:id/messages/text |
Send text |
POST |
/api/v1/sessions/:id/messages/image |
Send image |
POST |
/api/v1/sessions/:id/messages/video |
Send video |
POST |
/api/v1/sessions/:id/messages/audio |
Send audio |
POST |
/api/v1/sessions/:id/messages/document |
Send document |
POST |
/api/v1/sessions/:id/messages/sticker |
Send sticker |
POST |
/api/v1/sessions/:id/messages/location |
Send location |
POST |
/api/v1/sessions/:id/messages/contact |
Send contact |
POST |
/api/v1/sessions/:id/messages/edit |
Edit message |
POST |
/api/v1/sessions/:id/messages/react |
React to message |
Contacts
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/sessions/:id/contacts/check |
Check on WhatsApp |
POST |
/api/v1/sessions/:id/contacts/info |
Get contact info |
GET |
/api/v1/sessions/:id/contacts/:jid/picture |
Get profile picture |
POST |
/api/v1/sessions/:id/contacts/users |
Get user info |
Groups
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/sessions/:id/groups |
List groups |
GET |
/api/v1/sessions/:id/groups/:jid |
Get group |
GET |
/api/v1/sessions/:id/groups/:jid/info |
Get group info |
Presence & Chat State
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/sessions/:id/presence/set |
Set presence |
POST |
/api/v1/sessions/:id/chatstate/send |
Send chat state |
POST |
/api/v1/sessions/:id/chatstate/typing |
Send typing |
Blocking
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/sessions/:id/blocking/list |
Get block list |
POST |
/api/v1/sessions/:id/blocking/block |
Block contact |
POST |
/api/v1/sessions/:id/blocking/unblock |
Unblock contact |
GET |
/api/v1/sessions/:id/blocking/check/:jid |
Check if blocked |
Media
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/sessions/:id/media/upload |
Upload media |
Webhooks
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/sessions/:id/webhooks |
List webhooks |
POST |
/api/v1/sessions/:id/webhooks |
Register webhook |
DELETE |
/api/v1/sessions/:id/webhooks/:webhook_id |
Unregister webhook |
Other
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Health check |
GET |
/swagger-ui |
Swagger UI |
GET |
/api-docs/openapi.json |
OpenAPI spec |
Environment Variables
| Variable | Default | Description |
|---|---|---|
POSTGRES_HOST |
localhost | PostgreSQL host |
POSTGRES_PORT |
5432 | PostgreSQL port |
POSTGRES_USER |
postgres | PostgreSQL user |
POSTGRES_PASSWORD |
postgres | PostgreSQL password |
POSTGRES_DB |
wagateway | Database name |
JWT_SECRET |
(generated) | Token signing secret |
SUPERADMIN_TOKEN |
(generated) | Fixed admin token |
WHATSAPP_STORAGE_PATH |
./whatsapp_sessions | Session data directory |
RUST_LOG |
info | Log level (debug, info, warn, error) |
Troubleshooting
Session won't connect
- Check if the session exists:
GET /api/v1/sessions/:id - Try disconnecting first:
POST /api/v1/sessions/:id/disconnect - Then reconnect:
POST /api/v1/sessions/:id/connect - Check logs:
docker compose logs wa-rs
QR code not appearing
- Make sure session is in
connectingstate - Wait a few seconds after calling connect
- Refresh the QR endpoint
Webhooks not firing
- Verify the webhook URL is accessible from the server
- Check if events are configured correctly
- Verify signature validation isn't failing
Database connection failed
- Check if PostgreSQL is running
- Verify credentials in environment variables
- Make sure the database exists
What's Next
This is open source. MIT license. Do whatever you want with it.
GitHub: https://github.com/fdciabdul/wa-rs
Documentation: https://wa-rs.imtaqin.id/
Docker Hub: https://hub.docker.com/r/fdciabdul/wa-rs
Final Thoughts
I spent way too long fighting with Node.js memory leaks before I made this switch.
If you're running WhatsApp automation at scale, stop torturing yourself. Rust handles it better. Period.
The learning curve is real. But the payoff is worth it.
Star the repo if this helped. Open an issue if something's broken. PRs welcome.
Built by @taqin. Written in Rust because life's too short for garbage collection.
Comments