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

Pyppeteer does not expose a page-level event for every WebSocket message. To continuously print incoming responses, attach a Chrome DevTools Protocol (CDP) session to the page, enable the Network domain, subscribe to Network.webSocketFrameReceived, and keep the Python process alive. Map each event’s requestId to the URL reported by Network.webSocketCreated so output can be labeled or filtered.

Why page.on('response') cannot print WebSocket messages

Pyppeteer’s documented Page events cover HTTP request and response activity and page lifecycle changes. A WebSocket handshake is an HTTP exchange, but the application messages that follow it are frames on a persistent connection. The handshake response event therefore does not provide a stream of later server messages.

Chrome exposes those frames through its DevTools Protocol Network domain. Pyppeteer’s CDPSession lets you send protocol commands and subscribe to protocol events, which is the appropriate layer for continuous WebSocket observation.

Complete Pyppeteer example

The following script enables Network events before navigation, records socket URLs, prints every incoming frame, and remains alive after the initial page load. Replace the example URL with the page that opens your WebSocket.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import asyncio
from pyppeteer import launch

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['url']
        sockets[request_id] = url
        print(f'WebSocket opened: {url}', flush=True)

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

        if opcode == 1:
            print(f'<< {url}: {payload}', flush=True)
        else:
            print(
                f'<< {url}: binary payload '
                f'(opcode={opcode}): {payload}',
                flush=True,
            )

    client.on('Network.webSocketCreated', on_created)
    client.on('Network.webSocketFrameReceived', on_frame)

    try:
        await page.goto(
            'https://example.com',
            {'waitUntil': 'domcontentloaded'},
        )
        await asyncio.Event().wait()
    finally:
        await browser.close()

if __name__ == '__main__':
    asyncio.run(main())

Install Pyppeteer in the environment that will run the script with pip install pyppeteer. The first launch normally uses the Chromium revision managed by Pyppeteer. If you supply your own executable, verify that its CDP Network event schema matches the Pyppeteer release you installed.

What each part does

  • page.target.createCDPSession() creates a protocol session attached to this page target.
  • Network.enable turns on the Network domain. Without it, the WebSocket events are not available to this session.
  • Network.webSocketCreated supplies the socket’s requestId and URL. The dictionary preserves that relationship when several sockets are open.
  • Network.webSocketFrameReceived fires for incoming WebSocket messages. The callback reads its response object and prints the payload.
  • flush=True makes each line visible immediately when stdout is piped to a terminal, log collector, or another process.
  • asyncio.Event().wait() prevents the program from exiting after navigation. WebSocket traffic can continue indefinitely after page.goto() resolves.

Use the event fields correctly

Associate frames with the right socket

Every frame event carries a requestId. Keep the mapping created by webSocketCreated when you need to distinguish a chat socket from a telemetry socket, or when you want the URL in log lines. If a frame arrives before your mapping is populated, the example prints <unknown socket> rather than dropping the message.

For a single known endpoint, add a filter inside on_frame:

if not url.startswith('wss://stream.example.test/updates'):
    return

Filter by the mapped URL rather than by a guessed request number; request IDs are assigned by the browser and are not stable between runs.

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

Handle text and binary payloads differently

CDP uses opcode 1 for a text frame and reports its payloadData as a UTF-8 string. Other opcodes are represented as base64-encoded data. Printing a binary payload as if it were ordinary text can produce misleading output, so label it as binary or decode it according to the site’s protocol.

For a quick base64 decode, replace the binary branch with:

import base64

raw = base64.b64decode(payload)
print(f'<< {url}: {raw!r}', flush=True)

Decoding bytes is not the same as understanding the application message. A site may put JSON, compression, a protobuf, or another binary format inside the frame. Use the format defined by that application before attempting to parse it.

Capture outgoing frames when needed

The procedure above prints server-to-browser traffic only. To inspect messages sent by the page, subscribe to the corresponding event as well:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
def on_sent(event):
    request_id = event['requestId']
    frame = event.get('response', {})
    url = sockets.get(request_id, '<unknown socket>')
    print(f'>> {url}: {frame.get("payloadData", "")}', flush=True)

client.on('Network.webSocketFrameSent', on_sent)

Keep sent and received prefixes distinct in logs so a request is not mistaken for a server response.

Or skip the browser setup

If you need a rendered image or PDF of a page rather than a live WebSocket-frame stream, ScreenshotNeo provides a single-request screenshot API. It is not a replacement for CDP WebSocket events, but it can remove the browser-launch and page-capture code from a separate screenshot job. The API accepts cookies and consent banners like a visitor, removes more than 60 known consent platforms plus newsletter popups and chat widgets before capture, and bills only clean shots; bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed. 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. A cURL request is:

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

The equivalent Python request is:

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)

In 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 feature is included on every plan: full-page and element captures, device and retina settings, custom CSS or JavaScript, waits, request blocking, authentication headers, cookies, geolocation, PDF output, caching, signed links, asynchronous webhooks, bulk capture, and a usage API. The free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to try a screenshot workflow without installing Chromium.

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

Register listeners before navigation

Attach both listeners and call Network.enable before page.goto() or before code on the page can open its socket. Otherwise, a fast application can create the connection and deliver initial frames before your handlers exist.

  1. Launch Chromium or connect to it through Pyppeteer.
  2. Create the CDP session for the target page.
  3. Send Network.enable on that session.
  4. Register webSocketCreated and webSocketFrameReceived callbacks.
  5. Navigate, or trigger the page action that opens the socket.
  6. Keep the event loop running until your capture condition is met.

Use the page’s normal navigation and readiness waits independently from WebSocket collection. A page can finish loading while the socket remains active, so a successful navigation is not a signal to stop listening.

Stop after a condition instead of running forever

For a bounded capture, replace the never-ending event with an asyncio.Event that the callback sets. This example stops after ten incoming frames:

received = 0
finished = asyncio.Event()

def on_frame(event):
    global received
    received += 1
    frame = event.get('response', {})
    print(frame.get('payloadData', ''), flush=True)
    if received >= 10:
        finished.set()

client.on('Network.webSocketFrameReceived', on_frame)
await finished.wait()

In production code, avoid a module-level counter when several capture tasks share a process; store the count in an object or closure associated with one page and one CDP session.

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

Troubleshooting

No WebSocket output appears

  • Check that await client.send('Network.enable') completed without an exception.
  • Confirm that listeners were registered before navigation or the user action that opens the connection.
  • Verify that the page actually opens a WebSocket. Some applications use Server-Sent Events, long polling, WebTransport, or a socket created only after login.
  • Make sure the process is still alive. Returning from main() closes the browser and removes the event loop that would receive frames.

Only an HTTP response is printed

That usually means the code is listening to page.on('response'). Keep that listener if you need handshake diagnostics, but use Network.webSocketFrameReceived for application messages.

The URL is shown as unknown

A frame was handled without a matching entry in your dictionary, or the socket was created before the listeners were attached. Register webSocketCreated first and attach it before navigation. For diagnostics, print the frame’s requestId and inspect whether the created event was observed for the same page target.

Binary output looks unreadable

That is expected for a non-text opcode. Treat payloadData as base64, decode it to bytes, and then apply the application’s documented codec. Do not call json.loads() on every frame without checking the opcode and payload format.

The script misses the first messages

Network events are target-specific. Create the session on the exact Page that owns the socket, enable the Network domain, and install callbacks before any navigation, reload, or click that can start the connection.

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.

The CDP call or event name fails

Pyppeteer and Chromium versions must agree closely enough to expose the same protocol method and event fields. Pyppeteer 0.0.25 recommends its bundled Chromium and does not guarantee behavior with arbitrary browser versions; Chrome’s tip-of-tree protocol also does not promise backward compatibility. Check the installed Pyppeteer release and the Chromium executable it controls, then verify the Network event names and payload fields for that combination.

The browser closes during a long capture

Keep a reference to the browser, handle cancellation, and close it in a finally block. Also check for external process limits, container shutdown, and memory pressure. A long-running page can accumulate DOM state even when the frame callback itself is small.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance and reliability considerations

Printing can become the bottleneck

High-frequency sockets can generate more output than a terminal or file system can consume. For light debugging, direct print() is adequate. For sustained traffic, put events into an asyncio.Queue, have a separate consumer write structured records, and choose a queue limit so memory cannot grow without bound. If dropping old messages is acceptable, use a bounded queue and record when overflow occurs.

Reduce work in the callback

Keep the event handler short: read the request ID, opcode, and payload, apply a cheap URL filter, and hand the record to a worker. Avoid network calls, expensive decompression, or large synchronous transformations in the callback. Preserve the raw payload when later analysis may require the original bytes.

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.

Plan shutdown and retention

Stop on a deliberate condition, cancellation, or signal rather than killing Chromium abruptly. Close the browser in finally, flush your output sink, and remove per-socket entries when you observe Network.webSocketClosed if a long-lived process opens many connections. For diagnostics, retain timestamps, direction, socket URL, opcode, and request ID alongside the payload; redact credentials or personal data before storing logs.

Understand what this observes

The CDP event gives you the frame payload visible to the browser. It does not turn an application protocol into semantic records, repair malformed data, or infer whether a message is a reply to a particular request. Correlating requests and responses requires knowledge of the site’s own message IDs and protocol rules.

Useful lifecycle and error events

When diagnosing disconnects, add the related Network events:

  • Network.webSocketClosed identifies a socket that has ended.
  • Network.webSocketFrameError reports a frame-level error condition.
  • Network.webSocketFrameSent records browser-to-server frames.

Use these alongside the created and received events to build a complete connection log. A close event is not necessarily an application failure; servers may close idle connections normally, so interpret the reason and timing according to the site’s protocol.

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

Compatibility checklist

  • Use the Pyppeteer version and Chromium build that your project supports, preferably the bundled browser unless you have verified an external executable.
  • Confirm that page.target.createCDPSession() exists in the installed release and returns a session for the intended page target.
  • Confirm the Network event names and fields against the Chromium protocol version in use.
  • Test text and binary messages separately if the site supports both.
  • Test reconnects, multiple simultaneous sockets, and a clean shutdown before relying on the logger operationally.

Frequently Asked Questions

Can this script reconstruct a complete business-level response from several frames?

Not automatically. CDP reports WebSocket payloads, while message correlation, fragmentation rules, compression, and application-level envelopes belong to the website’s protocol. You must implement those rules if a logical response spans multiple payloads.

Does enabling the Network domain record traffic from every tab?

No. A CDP session created from one page target observes that target. Create and configure a session for each page whose WebSocket activity you need to inspect.

Can I use the same approach after a socket reconnects?

Yes, provided the listeners remain attached to the page session. Treat each new webSocketCreated event as a new request ID and update your mapping instead of assuming the original ID will be reused.

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.