DEV Community

Cover image for I Built a WhatsApp Gateway in Rust Because Node.js Wasn't Cutting It
Taqin
Taqin

Posted on

I Built a WhatsApp Gateway in Rust Because Node.js Wasn't Cutting It

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
Enter fullscreen mode Exit fullscreen mode

Getting Started (It's Stupid Simple)

Option 1: Docker (Recommended)

Pull from Docker Hub:

docker pull fdciabdul/wa-rs:latest
Enter fullscreen mode Exit fullscreen mode

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:
Enter fullscreen mode Exit fullscreen mode

Run it:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
    }
  }'
Enter fullscreen mode Exit fullscreen mode

Connect a Session

curl -X POST http://localhost:3451/api/v1/sessions/my-session/connect \
  -H "Authorization: Bearer YOUR_TOKEN"
Enter fullscreen mode Exit fullscreen mode

Get QR Code

curl http://localhost:3451/api/v1/sessions/my-session/qr \
  -H "Authorization: Bearer YOUR_TOKEN"
Enter fullscreen mode Exit fullscreen mode

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"}'
Enter fullscreen mode Exit fullscreen mode

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!"
  }'
Enter fullscreen mode Exit fullscreen mode

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!"
  }'
Enter fullscreen mode Exit fullscreen mode

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"
  }'
Enter fullscreen mode Exit fullscreen mode

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
  }'
Enter fullscreen mode Exit fullscreen mode

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"
  }'
Enter fullscreen mode Exit fullscreen mode

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"
  }'
Enter fullscreen mode Exit fullscreen mode

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"
  }'
Enter fullscreen mode Exit fullscreen mode

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"}
      ]
    }
  }'
Enter fullscreen mode Exit fullscreen mode

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"
  }'
Enter fullscreen mode Exit fullscreen mode

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": "👍"
  }'
Enter fullscreen mode Exit fullscreen mode

Disconnect a Session

curl -X POST http://localhost:3451/api/v1/sessions/my-session/disconnect \
  -H "Authorization: Bearer YOUR_TOKEN"
Enter fullscreen mode Exit fullscreen mode

Delete a Session

curl -X DELETE http://localhost:3451/api/v1/sessions/my-session \
  -H "Authorization: Bearer YOUR_TOKEN"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

You'll see something like:

┌─────────────────────────────────────────────────────────────┐
│  SUPERADMIN TOKEN                                            │
├─────────────────────────────────────────────────────────────┤
│  eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzdXBlcmFkbWluIiwicm9sZSI... │
├─────────────────────────────────────────────────────────────┤
│  Tip: Set SUPERADMIN_TOKEN in .env to use a fixed token     │
└─────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Using Swagger UI

  1. Go to http://localhost:3451/swagger-ui
  2. Click the Authorize button (top right)
  3. Paste your token
  4. 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
Enter fullscreen mode Exit fullscreen mode

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!"
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"]
  }'
Enter fullscreen mode Exit fullscreen mode

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"]
  }'
Enter fullscreen mode Exit fullscreen mode

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"]
  }'
Enter fullscreen mode Exit fullscreen mode

Get Profile Picture

curl http://localhost:3451/api/v1/sessions/my-session/contacts/628123456789@s.whatsapp.net/picture \
  -H "Authorization: Bearer YOUR_TOKEN"
Enter fullscreen mode Exit fullscreen mode

List Groups

curl http://localhost:3451/api/v1/sessions/my-session/groups \
  -H "Authorization: Bearer YOUR_TOKEN"
Enter fullscreen mode Exit fullscreen mode

Get Group Info

curl http://localhost:3451/api/v1/sessions/my-session/groups/123456789@g.us/info \
  -H "Authorization: Bearer YOUR_TOKEN"
Enter fullscreen mode Exit fullscreen mode

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"
  }'
Enter fullscreen mode Exit fullscreen mode

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"
  }'
Enter fullscreen mode Exit fullscreen mode

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"
  }'
Enter fullscreen mode Exit fullscreen mode

Options: composing, recording, paused


Blocking

Get Block List

curl http://localhost:3451/api/v1/sessions/my-session/blocking/list \
  -H "Authorization: Bearer YOUR_TOKEN"
Enter fullscreen mode Exit fullscreen mode

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"
  }'
Enter fullscreen mode Exit fullscreen mode

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"
  }'
Enter fullscreen mode Exit fullscreen mode

Check if Blocked

curl http://localhost:3451/api/v1/sessions/my-session/blocking/check/628123456789@s.whatsapp.net \
  -H "Authorization: Bearer YOUR_TOKEN"
Enter fullscreen mode Exit fullscreen mode

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

  1. Check if the session exists: GET /api/v1/sessions/:id
  2. Try disconnecting first: POST /api/v1/sessions/:id/disconnect
  3. Then reconnect: POST /api/v1/sessions/:id/connect
  4. Check logs: docker compose logs wa-rs

QR code not appearing

  1. Make sure session is in connecting state
  2. Wait a few seconds after calling connect
  3. Refresh the QR endpoint

Webhooks not firing

  1. Verify the webhook URL is accessible from the server
  2. Check if events are configured correctly
  3. Verify signature validation isn't failing

Database connection failed

  1. Check if PostgreSQL is running
  2. Verify credentials in environment variables
  3. 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.

Top comments (0)