# Pawl: a promise-based WebSocket client for PHP

> Pawl is the client half of the Ratchet project, a PHP WebSocket client that returns a promise from the connector and hands you a WebSocket object with send and close. It is built on ReactPHP and passes the Autobahn test suite, but its version numbering has sat at 0.4.x for years.

**ratchetphp/Pawl** — Asynchronous WebSocket client

- Repository: https://github.com/ratchetphp/Pawl
- Stars: 612 · Forks: 98
- Language: PHP
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/ratchetphp-pawl

## The connector returns a promise, and that shapes the whole API

Pawl is an asynchronous WebSocket client in PHP, and the design decision that defines it is that a connection attempt returns a promise. The standalone example in the README is short enough to show the whole shape:

```php
<?php

require __DIR__ . '/vendor/autoload.php';

\Ratchet\Client\connect('wss://echo.websocket.org:443')->then(function($conn) {
    $conn->on('message', function($msg) use ($conn) {
        echo "Received: {$msg}\n";
        $conn->close();
    });

    $conn->send('Hello World!');
}, function ($e) {
    echo "Could not connect: {$e->getMessage()}\n";
});
```

The static connect helper is called with a URL, and the promise resolves with the connection object or rejects with the error. Inside the success handler you register an event listener for the message event, and the callback captures the connection with use so it can close the socket once a reply arrives. This is the pattern that distinguishes Pawl from a synchronous WebSocket client in another language. Nothing blocks, and there is no read loop you have to write. The same shape appears in the more detailed example at the end of the README, where the failure branch stops the event loop explicitly with $loop->stop(), which is the detail that tells you what happens if you forget to handle the error path.

## Three classes, and the connector takes the configuration arguments

The README names three primary classes to be aware of. The Connector makes HTTP requests to servers, returning a promise that resolves to a WebSocket object if the handshake succeeds. A connector is configured through its constructor, a request is made by invoking the class, and multiple connections can be established through a single connector, so one configured connector can open many sockets without being rebuilt. Invoking it takes three parameters. The url is a string, a valid URI starting with ws:// or wss://, and it also accepts a PSR-7 Uri object. The subProtocols parameter is an optional indexed array of WebSocket sub-protocols to negotiate, and the documentation is explicit that the connection fails if the client and the server cannot agree on one whenever any are listed. The headers parameter is an optional associative array of additional headers for the handshake, and the README names Origin as a common one to set. That third parameter is the practical reason to construct a Connector yourself rather than use the static helper, since the static connect call has nowhere to pass handshake headers.

## WebSocket gives you send, close and the handshake objects

The WebSocket object is what you interact with once connected, and the README is precise about its surface. It has two methods, send and close. It has two public properties, request and response, which are PSR-7 objects representing the client and server side HTTP handshake headers used to establish the connection. That detail is more useful than it first appears, because the handshake is where a WebSocket client is often refused, whether through a proxy, an origin check or a sub-protocol mismatch, and having both sides of the exchange available as PSR-7 objects means you can inspect what actually happened rather than guessing. Being PSR-7 objects also means they slot into the same HTTP abstractions the rest of a ReactPHP application uses. The third class is the Message, the object received from the server. It has a __toString method, which the README says is how you will most often want to access the data received. If you need binary messaging, the README says you will most likely need to use methods on the object instead, since the string conversion of a binary frame is not the interesting part. The typed handler in the longer example makes this concrete, taking a \Ratchet\RFC6455\Messaging\MessageInterface argument in the message callback.

## The explicit example shows what you give up with the helper

The second example in the README is the one to read if you need control, and it differs from the first in three ways. It builds a ReactPHP socket connector explicitly, with a dns setting of 8.8.8.8 and a timeout of 10:

```php
$reactConnector = new \React\Socket\Connector([
    'dns' => '8.8.8.8',
    'timeout' => 10
]);
$loop = \React\EventLoop\Loop::get();
$connector = new \Ratchet\Client\Connector($loop, $reactConnector);

$connector('ws://127.0.0.1:9000', ['protocol1', 'subprotocol2'], ['Origin' => 'http://localhost'])
->then(function(\Ratchet\Client\WebSocket $conn) {
    $conn->on('message', function(\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($conn) {
        echo "Received: {$msg}\n";
        $conn->close();
    });

    $conn->on('close', function($code = null, $reason = null) {
        echo "Connection closed ({$code} - {$reason})\n";
    });

    $conn->send('Hello World!');
}, function(\Exception $e) use ($loop) {
    echo "Could not connect: {$e->getMessage()}\n";
    $loop->stop();
});
```

First, you get the event loop as a concrete object, here the static Loop::get(), which means you own the loop's lifetime and can stop it. Second, you get to configure the underlying socket connector, so the DNS resolver and the connection timeout are yours to set rather than the library's defaults. Third, the invocation passes all three parameters, sub-protocols and an Origin header included. The close handler is also registered here, printing the close code and reason, which the first example does not do and which you will want in anything that runs unattended.

## The Autobahn badge is the claim worth checking

The README leads with a badge pointing at the Autobahn Testsuite report for Pawl, marked as passing. The Autobahn suite is the standard conformance and fuzzing test set for WebSocket implementations, so this is a more meaningful signal than a unit test count, because it exercises handshake edge cases and malformed frames that a hand written test suite usually skips. The badge links to a report hosted at socketo.me, which is the address used by the Ratchet project. There is also a CI status badge for the GitHub Actions workflow, and the repository layout is small: a src/ directory, a tests/ directory, composer.json, phpunit.xml.dist alongside phpunit.xml.legacy, and a LICENSE file. The legacy phpunit configuration is a detail worth noting, since it suggests the test harness itself has been migrated at some point and the old file is kept around. The licence is MIT. The project publishes no GitHub releases under its own name, but the Composer package is the distribution channel, and installation is a single command:

```bash
    composer require ratchet/pawl
```

## A client without a server, and a version line that has not moved far

Two limitations define where Pawl fits. The first is scope. This is a client library, and the README only ever describes connecting to a server. If you need to accept WebSocket connections and hold state per client, that is the other half of the Ratchet project, a separate codebase, and Pawl is not it. Choosing Pawl for a server-side task means adopting one library and then writing the server yourself, which is exactly the work the other project exists to save. The second is the release cadence. The listed releases are v0.4.3 on 2025-03-19, v0.4.2 on 2025-03-02, and v0.4.1 on 2021-12-10. So the 0.4 line spans more than three years, and the last push to the repository was on 2026-07-28, after the newest tagged release. The topics say async, websocket-client and websockets, and the primary language is PHP. For a library this small and this stable, a slow release line is not automatically a problem, but it does mean you should check what the repository has that the tag does not, especially for anything touching the TLS or framing layer. The alternative approach, in a different language or with a different event loop, is a client like the ones built on top of a general purpose async runtime, which will track protocol and dependency updates more often but will not give you the PSR-7 handshake objects or the Ratchet ecosystem.

## Conclusion

Pawl suits a PHP developer who needs a WebSocket client with a promise-based connection API, who is already using ReactPHP for the event loop, and who wants a small, focused library rather than a full framework. It is the wrong choice if you need a server, since Pawl is the client side only and the Ratchet server is a separate project, and it is the wrong choice if you need the latest protocol or library versions, because the newest listed release is v0.4.3 from 2025-03-19 while the last push was on 2026-07-28. Verify first that your ReactPHP version matches what Pawl expects, that the sub-protocol negotiation behaviour suits your server, and that the 0.4.x line still has the security and protocol updates your use of it requires.

## FAQ

### How do I install the Pawl WebSocket client?

Through Composer, with the command composer require ratchet/pawl. The project is an MIT licensed PHP library published by the Ratchet project.

### How do I connect to a WebSocket server with Pawl?

Use the static connect helper with a ws:// or wss:// URL and handle the promise. The success callback receives the WebSocket object, and the failure callback receives the exception, as shown in the README example against the echo server at wss://echo.websocket.org:443.

### What are the main classes in Pawl?

Three: Connector, which makes the handshake request and returns a promise resolving to a WebSocket; WebSocket, which has send and close methods plus request and response PSR-7 properties for the handshake; and Message, the object received from the server, whose __toString is the usual way to read the data.

### Does Pawl let me set request headers and sub-protocols?

Yes. The Connector invocation takes an optional array of sub-protocols to negotiate and an optional associative array of additional handshake headers, with Origin named as a common one to set. The connection fails if the client and server cannot agree on a sub-protocol when any are provided.

## Sources

- [Issues](https://github.com/ratchetphp/Pawl/issues)
- [License: MIT](https://github.com/ratchetphp/Pawl/blob/master/LICENSE)
- [ratchetphp/Pawl on GitHub](https://github.com/ratchetphp/Pawl)
- [README](https://github.com/ratchetphp/Pawl/blob/master/README.md)
- [Releases](https://github.com/ratchetphp/Pawl/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ratchetphp-pawl
