• Home
  • Blog
  • whatsapp-web.js: A Practical Guide to the Puppeteer Library
Libraries
10 min read Jun 15, 2026

whatsapp-web.js: A Practical Guide to the Puppeteer Library

How whatsapp-web.js drives WhatsApp Web through Puppeteer — auth strategies, events, sending media, common errors, and deployment, with honest ToS guidance.

Key takeaways
  • whatsapp-web.js (npm: whatsapp-web.js, Apache-2.0, Node 18+) drives the real WhatsApp Web app inside headless Chromium via Puppeteer — it is an unofficial client, not Meta's Cloud API.
  • Choose an auth strategy: NoAuth (re-scan every time), LocalAuth (needs a persistent disk), or RemoteAuth (Mongo/S3 store for ephemeral or containerized hosts).
  • Use events (ready, message, message_create, disconnected) plus sendMessage and MessageMedia to build reactive bots; Buttons and Lists are deprecated and no longer work.
  • The high-traffic failure 'Execution context was destroyed' usually traces to version drift — keep the library on its newest release and add reconnect logic.
  • Automation violates WhatsApp's ToS and risks bans; use it for personal/prototype/reactive tooling, and move to the official Cloud API for production business messaging.

What whatsapp-web.js actually is

whatsapp-web.js is a Node.js library that automates WhatsApp by remote-controlling the real WhatsApp Web application. Created and maintained by Pedro S. Lopez, it is published on npm as whatsapp-web.js, licensed under Apache 2.0, and requires Node.js 18 or higher. It is actively maintained — the 1.34.x line shipped in 2026 — and it now lives under the wwebjs GitHub organization. If you see both github.com/pedroslopez/whatsapp-web.js and github.com/wwebjs/whatsapp-web.js, they are the same repository, so issue and PR numbers are shared across both URLs.

The important part to understand up front is the mechanism. There is no secret protocol implementation here. The library launches a headless Chromium browser through Puppeteer, loads web.whatsapp.com, and then calls WhatsApp Web's own internal JavaScript functions (its 'store') through page evaluation. In other words, your code is a thin wrapper around the same web app you would open in a browser tab. That design choice explains both its strengths (it inherits whatever WhatsApp Web can do) and its fragility (when WhatsApp ships an internal update, selectors and store functions can change underneath you).

How it works under the hood

Because the library drives WhatsApp Web, it relies on WhatsApp's Multi-Device feature. You link it the same way you link WhatsApp Web on a laptop: by scanning a QR code with the phone app. After linking, the phone does not have to stay online continuously, but the linked-device session must remain valid — if you unlink the device or the session expires, the client stops working until you re-authenticate.

Puppeteer downloads a bundled Chromium build the first time you install. Budget for a sizable download — it has typically landed in the low hundreds of megabytes, though the exact size shifts with each Chromium-for-Testing release that Puppeteer pins, so treat any single number as a moving target rather than a constant. That bundled Chromium handles almost everything, with one notable exception: video media. Chromium ships without the proprietary codecs needed for some video formats, so to send or receive video reliably you should point Puppeteer at a real Google Chrome install via the executablePath option.

JS
const { Client, LocalAuth } = require('whatsapp-web.js');

const client = new Client({
  authStrategy: new LocalAuth(),
  puppeteer: {
    // Use real Chrome for video media; bundled Chromium lacks the codecs.
    // NOTE: this path differs per OS — adjust for your environment:
    //   Linux:   '/usr/bin/google-chrome-stable'
    //   macOS:   '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
    //   Windows: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
    executablePath: '/usr/bin/google-chrome-stable',
    headless: true,
    args: ['--no-sandbox', '--disable-setuid-sandbox'],
  },
});

Each client is effectively a full browser instance, so sessions are heavy. Plan for one Chromium process per linked number, not dozens of cheap connections — this is the single biggest difference between whatsapp-web.js and protocol-level libraries, and it shapes how you deploy.

Install and a minimal bot

BASH
npm i whatsapp-web.js qrcode-terminal

qrcode-terminal is optional but handy: it renders the login QR directly in your terminal so you can scan it with your phone. Here is a complete reactive bot that authenticates, waits for the ready event, and replies to incoming messages.

JS
const { Client, LocalAuth } = require('whatsapp-web.js');
const qrcode = require('qrcode-terminal');

const client = new Client({ authStrategy: new LocalAuth() });

client.on('qr', (qr) => qrcode.generate(qr, { small: true }));
client.on('ready', () => console.log('Client is ready'));

client.on('message', async (msg) => {
  if (msg.body === '!ping') {
    await msg.reply('pong');
  }
});

client.on('disconnected', (reason) => {
  console.log('Disconnected:', reason);
  client.initialize(); // simple reconnect
});

client.initialize();

Note the difference between two similar events: message fires for incoming messages, while message_create fires for every message created on the account, including the ones you send yourself. Use message for a classic responder and message_create when you also need to observe your own outgoing traffic.

Session strategies: NoAuth, LocalAuth, RemoteAuth

Authentication is handled by pluggable strategies that all extend an abstract AuthStrategy class. Picking the right one is the most consequential early decision, because it determines whether your bot survives restarts and whether it can run on ephemeral infrastructure.

StrategyWhere the session livesBest forKey options
NoAuth (default)Nowhere — not persistedQuick tests; you scan a QR every start
LocalAuthLocal filesystemSingle long-running host with a persistent diskdataPath, clientId
RemoteAuthRemote DB via a Store pluginContainers, ephemeral hosts, multi-instancestore, backupSyncIntervalMs

LocalAuth writes the session to disk, with a clientId option so you can run multiple isolated sessions side by side. The catch: it needs a genuinely persistent filesystem. On hosts that wipe the disk between deploys or restarts (Heroku, many serverless and container setups), LocalAuth will silently lose the session and force a fresh QR scan every boot.

RemoteAuth solves that by saving the Multi-Device session to a remote database through a Store plugin and backing it up on an interval. Official stores include wwebjs-mongo (MongoStore, which needs mongoose) and wwebjs-aws-s3 for S3. It fires a remote_session_saved event roughly a minute after the first QR scan, and you can implement a custom store against the defined interface if you use a different backend.

JS
const { Client, RemoteAuth } = require('whatsapp-web.js');
const { MongoStore } = require('wwebjs-mongo');
const mongoose = require('mongoose');

await mongoose.connect(process.env.MONGODB_URI);
const store = new MongoStore({ mongoose });

const client = new Client({
  authStrategy: new RemoteAuth({
    store,
    backupSyncIntervalMs: 300000, // back up every 5 minutes
  }),
});

client.on('remote_session_saved', () => console.log('Session persisted to Mongo'));
client.initialize();

Sending messages, media, and managing groups

You send everything through client.sendMessage(chatId, content, options). Chat IDs follow a fixed format: individuals are <number>@c.us and groups are <id>@g.us. Before messaging a number you have never contacted, verify it is actually on WhatsApp with client.getNumberId(number), which returns null when the number is not registered and otherwise hands back the correct @c.us ID.

JS
const { MessageMedia } = require('whatsapp-web.js');

// 1) Verify the number is on WhatsApp (use sparingly — see note above)
const numberId = await client.getNumberId('15551234567');
if (!numberId) throw new Error('Not on WhatsApp');

// 2) Send text
await client.sendMessage(numberId._serialized, 'Hi there');

// 3) Send an image with a caption
const media = MessageMedia.fromFilePath('./invoice.jpg');
await client.sendMessage(numberId._serialized, media, { caption: 'Your invoice' });

// You can also build media from a URL or raw base64:
// const media = await MessageMedia.fromUrl('https://example.com/a.pdf');
// const media = new MessageMedia(mimetype, base64Data, filename);

Reading the account works through client.getChats(), client.getContacts(), and chat.fetchMessages(). Group administration is well covered: client.createGroup(title, participants) plus chat.addParticipants, removeParticipants, promoteParticipants, and demoteParticipants. Also supported are stickers, contact cards, location, replies, mentions, reactions, polls and voting, and Channels.

Common errors and how to fix them

Most production pain with whatsapp-web.js traces back to one root cause: version drift between the library and whatever WhatsApp Web build is currently served. Because the library calls WhatsApp Web's internal functions, an internal WA Web update can break selectors and store calls. The single most effective defense is to run the newest release of whatsapp-web.js.

Error / symptomTypical causeFix
Execution context was destroyed (within seconds of scanning the QR)WhatsApp Web navigates/reloads while the library injects its version-detection code right after authentication; reported on standard and Business accounts (see issues #3792 and #2897)Upgrade to the latest version; pin a known-good WA Web version if needed; retry initialization on this error
Execution context was destroyed (after a long-running session)Community-reported drift where WA Web navigates the page out from under a session that has been alive for hoursListen on disconnected, wrap calls in try/catch, and destroy + re-initialize the client rather than assuming the session lives forever
Ready event never fires after authenticationLibrary out of sync with a specific WA Web build (e.g. 2.3000.x)Upgrade the library
getLastMsgKeyForAction is not a functionA WA Web internal change inside sendSeenUpgrade the library
Failed to launch the browser process (Docker/headless)Missing Chromium libs or sandbox restrictionsRun with args: ['--no-sandbox', '--disable-setuid-sandbox'] and install system libraries

'Execution context was destroyed' is the dominant recurring report in the issue tracker. It has two distinct flavors. The reproducible one in issues #3792 and #2897 fires almost immediately after the QR scan, while the library injects its version-detection code and the page navigates underneath it; upgrading (and, when necessary, pinning a known-good WA Web version) is the fix. Separately, the community reports the same message on sessions that have run for hours — there the page simply navigated out from under Puppeteer. Either way, the message rarely means your code is wrong, so build for it: listen on disconnected, wrap calls in try/catch, and re-initialize the client.

JS
process.on('unhandledRejection', (err) => {
  if (String(err).includes('Execution context was destroyed')) {
    console.warn('WA Web navigated — restarting client');
    client.destroy().finally(() => client.initialize());
  }
});

Deployment tips for headless and Docker

Running a full Chromium on a server is the trickiest operational part. A few concrete rules save a lot of debugging. Always launch with --no-sandbox and --disable-setuid-sandbox in containers. Linux and headless environments need Chromium's system libraries installed; minimal Docker base images usually lack them, which is the most common cause of launch failures.

  • Install the required apt libraries for Chromium in your image (fonts, libnss3, libatk, libgbm, and friends).
  • Use dumb-init (or tini) as PID 1 to reap zombie Chrome processes, which otherwise pile up over time.
  • Consider --cap-add=SYS_ADMIN when sandboxing causes crashes you cannot otherwise resolve.
  • Use LocalAuth only on a persistent volume; on ephemeral hosts switch to RemoteAuth with Mongo or S3.
  • Budget one browser per session — sessions are heavy, so scale horizontally with separate processes rather than many clients in one process.

One sizing note worth flagging before you commit to this architecture: if your actual need is checking whether numbers are on WhatsApp or pulling public profile data, a fleet of Chromium instances is the wrong shape — it is resource-hungry and, as noted earlier, repeated probing from a linked number is ban-prone. That class of work (registration status, profile picture, display name, about text, business flag) is read-only and stateless, so it belongs on a request/response service rather than a browser-per-session bot, regardless of which provider you use.

ToS, ban risk, and when to choose the official API

Be honest with yourself about compliance. The project's own disclaimer states it is not affiliated with WhatsApp and that it is not guaranteed you will not be blocked, because WhatsApp does not allow bots or unofficial clients. Automation through whatsapp-web.js violates WhatsApp's Terms of Service, and that carries real ban risk for the linked number.

Crucially, ban risk correlates far more with behavior than with the connection method. Bulk and cold outbound, spam-like sending patterns, and messaging people who never opted in are what get numbers flagged. Reactive bots that only reply to inbound messages report markedly lower ban rates. So if you do use the library, keep it inbound-driven and human-paced.

Recommend whatsapp-web.js for personal projects, prototypes, internal tooling, and reactive use — and explicitly avoid bulk or cold messaging.

For production business messaging, the compliant path is the official WhatsApp Business Platform (the Cloud API) through Meta or a Business Solution Provider. It is approved, supports templates and rich interactive components, and will not get your number banned for sanctioned use. The right tool depends on the job: prototype and personal automation lean toward whatsapp-web.js, while customer-facing business messaging at scale belongs on the Cloud API. For pure validation and enrichment — confirming a number is registered or pulling public profile data — a dedicated read-only API sidesteps both the browser overhead and the ban risk.

Frequently asked questions

Skip the browser fleet — verify numbers via API

Need to confirm a number is on WhatsApp or pull public profile data without running headless Chromium per session? Our hosted WhatsApp Profile API returns registration status, profile picture, display name, about text, and the business flag in one call.

Explore the API
Sponsored

Need cheap numbers to create, verify or warm up WhatsApp accounts for testing? GrizzlySMS rents virtual numbers for WhatsApp verification from under $1.

Get a WhatsApp number

Sources & further reading

Related

More from the blog

Written by
Eduardo Airaudo

Developer and founder of the WhatsApp Profile API. Building WhatsApp tooling and APIs since 2022.

What Our Users Say

Real reviews from our satisfied customers

4.5/5 (170 reviews)