Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Pyppeteer does not expose a documented page event for individual WebSocket messages. To continuously print incoming responses, attach a Chrome DevTools Protocol (CDP) session to the page, enable the Network domain, subscribe to Network.webSocketCreated and Network.webSocketFrameReceived, and keep the process alive after navigation. The frame event supplies the socket request ID and payload; the creation event lets you map that ID to a URL.

Why page.on('response') is not enough

Pyppeteer’s normal response event belongs to the HTTP request lifecycle. It can report the WebSocket handshake, but it does not stream the application messages exchanged after the connection opens. A WebSocket can remain open while the page continues receiving frames, so waiting for navigation or listening only for HTTP responses will miss the data you want.

CDP’s Network domain provides the per-frame events. Pyppeteer exposes the raw protocol through CDPSession.send() and its event-emitter interface. The practical sequence is:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  1. Create a page and a CDP session attached to that page target.
  2. Send Network.enable.
  3. Register listeners before navigation or before page code opens its socket.
  4. Record each socket’s URL from Network.webSocketCreated.
  5. Print incoming frames from Network.webSocketFrameReceived.
  6. Keep an awaitable task alive until you intentionally stop the program.

Complete Pyppeteer example

The following script prints text frames as they arrive and labels binary frames without incorrectly treating them as text. It also records close and frame-error events for diagnostics.

import asyncio
from contextlib import suppress
from pyppeteer import launch

TARGET = "https://example.com"

async def main():
    browser = await launch()
    page = await browser.newPage()
    client = await page.target.createCDPSession()
    sockets = {}

    await client.send("Network.enable")

    def on_created(event):
        request_id = event["requestId"]
        url = event.get("url", "<unknown socket>")
        sockets[request_id] = url
        print(f"WebSocket opened: {url}", flush=True)

    def on_received(event):
        request_id = event["requestId"]
        frame = event["response"]
        url = sockets.get(request_id, "<unknown socket>")
        opcode = frame.get("opcode")
        payload = frame.get("payloadData", "")

        if opcode == 1:
            # CDP defines opcode 1 as a UTF-8 text payload.
            print(f"<< {url}: {payload}", flush=True)
        else:
            # Non-text payloads are represented by CDP as base64 data.
            print(
                f"<< {url}: binary payload (opcode={opcode}): {payload}",
                flush=True,
            )

    def on_closed(event):
        request_id = event["requestId"]
        print(f"WebSocket closed: {sockets.get(request_id, request_id)}", flush=True)

    def on_error(event):
        request_id = event["requestId"]
        print(f"WebSocket frame error for {sockets.get(request_id, request_id)}: {event}", flush=True)

    client.on("Network.webSocketCreated", on_created)
    client.on("Network.webSocketFrameReceived", on_received)
    client.on("Network.webSocketClosed", on_closed)
    client.on("Network.webSocketFrameError", on_error)

    try:
        await page.goto(TARGET)
        # Navigation finishing does not close a live WebSocket.
        await asyncio.Event().wait()
    finally:
        with suppress(Exception):
            await client.detach()
        await browser.close()

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        pass

Install Pyppeteer in the environment that will run the script, then start it normally. Replace TARGET with the page that creates the socket. The listeners are installed before page.goto(), which avoids losing early creation or frame events.

How the CDP events fit together

Network.webSocketCreated

This event reports a requestId and the socket URL. Store the pair in a dictionary. A page may open several sockets, so a single global “current URL” is unsafe.

Network.webSocketFrameReceived

For every incoming frame, CDP supplies the same requestId plus a response object. Read response["payloadData"] and inspect response["opcode"] before decoding or printing it. Opcode 1 is text and is represented as a UTF-8 string. Other opcodes are represented as base64 data; decode them only when the site’s binary protocol is known.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Optional direction and lifecycle events

Subscribe to Network.webSocketFrameSent when you also need client-to-server frames. Use Network.webSocketClosed to remove a socket from your map or report normal shutdown, and Network.webSocketFrameError to expose protocol-level errors.

Filtering and parsing a continuous stream

Listen to one socket

Keep the URL map, then filter in the receive callback:

if not url.startswith("wss://feed.example/"):
    return

For a stronger filter, store additional metadata when the URL is created or maintain a set of accepted request IDs. Request IDs, rather than URLs, are the reliable identity for subsequent frame events.

Parse JSON text safely

import json

if opcode == 1:
    try:
        message = json.loads(payload)
    except json.JSONDecodeError:
        print(f"non-JSON text from {url}: {payload}")
    else:
        print(message)

A WebSocket frame is not automatically a complete business record. The application may put compressed data, multiplexed messages, or another binary format inside the payload. Interpret it according to that site’s protocol rather than assuming every text frame is JSON.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Handle binary payloads

CDP reports non-text payload data as base64. Convert it only when you know the expected format:

import base64

raw = base64.b64decode(payload)
# Pass raw to the site's documented binary decoder.

Do not call UTF-8 decoding blindly; it can corrupt or reject valid binary traffic.

Keeping the printer alive without leaking resources

After page.goto(), an ordinary script may reach its end and exit before the next frame arrives. An unresolved event, an application task, or an explicit shutdown signal keeps the loop running. The example uses asyncio.Event().wait() and closes both the CDP session and browser in finally.

In a service, replace the never-set event with an asyncio.Event controlled by your shutdown handler. On cancellation, detach the session, close the page or browser, and let the process terminate. Add bounded output, file rotation, or a queue if the site sends high-rate traffic; printing every frame synchronously can become the bottleneck.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Navigation, timing and reliability

  • Register first: create the CDP session, enable Network, and attach handlers before navigation or any script that opens the socket.
  • Do not equate page load with stream completion: WebSockets commonly continue after the load event.
  • Expect reconnects: a page can close one socket and create another. Treat each new requestId as a separate connection.
  • Preserve ordering per callback: send received events to a queue if downstream parsing or storage is slower than the browser.
  • Control log volume: redact tokens, cookies, authorization data, and personal information before writing frames to shared logs.
  • Use a realistic browser target: Pyppeteer 0.0.25 says it works best with its bundled Chromium and does not guarantee arbitrary Chromium versions. CDP tip-of-tree documentation also changes and does not promise backward compatibility. Verify event names and fields against the Chromium build you actually launch.

Troubleshooting

No frames appear

  • Confirm await client.send("Network.enable") completed before navigation.
  • Make sure listeners are attached to the page’s target session, not to a different page or browser target.
  • Check that the page really uses WebSockets; Server-Sent Events, polling, and WebTransport use different instrumentation.
  • Keep the process alive after navigation.

You see the handshake but not messages

The handshake is an HTTP response, not a frame stream. Use Network.webSocketFrameReceived and match its requestId to webSocketCreated.

The callback raises a missing-key error

Do not assume every event has identical fields. Use event.get() for optional values and retain a fallback such as <unknown socket>. Keep the frame object separate from the outer event: payload data is under event["response"]["payloadData"].

Output is unreadable

Check the opcode. Text is printable UTF-8; non-text data is base64 and requires the application’s decoder. Compression or a binary serialization format must be handled before attempting JSON parsing.

Events differ after a Chromium or Pyppeteer upgrade

Compare the installed versions and inspect the protocol exposed by that Chromium build. Pyppeteer’s session-creation method is commonly page.target.createCDPSession(), but confirm it for your installed release. Treat CDP event schemas as version-sensitive rather than assuming every browser revision is interchangeable.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your actual goal is a clean image or PDF of a page rather than observing its live socket traffic, ScreenshotNeo provides a single HTTP call. It accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups and chat widgets before capture; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.

See the ScreenshotNeo API documentation for all options, including full-page and selector captures, device and retina settings, custom CSS or JavaScript, waits, request blocking, headers, cookies, geolocation, caching, signed links, asynchronous jobs and bulk capture.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Every plan includes the features. The Free plan provides 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots, with yearly billing giving two months free. Create a free ScreenshotNeo account to start.

FAQ

Can Pyppeteer’s page emitter print WebSocket messages directly?

Not through its documented page-event list. Use the page-target CDP session and Network frame events.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Does webSocketFrameReceived guarantee a complete application message?

It reports a WebSocket message payload, but the application can encode multiple logical records, compression, or binary structures inside it. Parsing remains application-specific.

How can I capture messages sent by the browser?

Subscribe to Network.webSocketFrameSent and use the same request-ID mapping.

Why does the script stop immediately?

Navigation completion does not keep Python alive. Await a long-lived task or shutdown event, then close the browser deliberately when stopping.

Frequently Asked Questions

Can Pyppeteer’s page emitter print WebSocket messages directly?

Not through its documented page-event list. Use the page-target CDP session and Network frame events.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Does webSocketFrameReceived guarantee a complete application message?

It reports a WebSocket message payload, but application-level records and formats remain site-specific.

How can I capture messages sent by the browser?

Subscribe to Network.webSocketFrameSent and use the same request-ID mapping.

Why does the script stop immediately?

Keep the event loop alive with an awaitable task or shutdown event, then close the browser deliberately.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.