A Common Lisp thread‑safe WebSocket client library.
wscli is made for the use case where a long-running Lisp process must continuously
connects and disconnects to multiple sockets. None of the existing web socket client libraries
are satisfactory for this use case as of 2026.
Although there are a few Common Lisp web socket libraries, most of them focus on server but not
the client side. Among web socket clients on the market, Allergo’s aserve client seems to be the most
mature but it is not open source. wet-sprockets uses global variable and looks a bit incomplete.
While fukamachi’s popular websocket-driver contains a performant client that works excellently for
most typical use, it is less designed with concurrency and robust long-running services in mind
compared to wscli. For example, wscli provides better thread-safety for multiple threads
writing to the same socket and some request closing; wscli also does safer gentle shutdown
(the user’s handler code can be in a foreign library doing any side-effects and we should wait
for it to finish).
Establish a WebSocket connection. Returns the connection object.
host,port– server address.path– resource path, e.g."/chat".handler– a function of two arguments (type, data) called for incoming messages.background– if true (default) starts the listener in a separate thread.origin– optionalOriginheader.protocols– list of subprotocol strings to offer.secure–tfor WSS (TLS).verify– passed tocl+ssl:make-ssl-client-stream(defaultnil).hostname– optional SNI hostname (defaults tohost).
Parses ws://... or wss://... URLs and calls connect.
All other keyword arguments are passed through.
The handler function receives two values:
- Event type – a keyword:
:text,:binary,:ping,:pong,:error, or:close. - Payload – a string for
:text, a(simple-array (unsigned-byte 8))for:binary,:ping, or:pong, a condition for:error, and(close-code . reason-string)for:close.
For example:
(defun my-handler (type data)
(format t "~&[WS] ~A: ~S~%" type data))Initiate the WebSocket closing handshake (non‑blocking).
Default close code is 1000 (normal closure).
reason is a UTF‑8 string, truncated to ≤ 123 bytes.
After calling this, the connection state becomes :closing and will eventually transition to :closed (either after the peer replies or after the timeout).
Block the calling thread until the connection reaches :closed.
timeout in seconds, nil means wait forever. Returns t on success, nil on timeout.
Send a text frame. String is encoded as UTF‑8.
Send a binary frame. Octets must be a (unsigned-byte 8) array.
Send a ping control frame. Payload ≤ 125 bytes. The listener automatically replies with a pong.
If the connection was created with :background nil, call this function in your thread to start the reading loop (blocks until the connection closes).
(defun handler (type data)
(print (list type data)))
(defparameter *conn* (connect "echo.websocket.org" 443 "/"
:secure t
:handler #'handler))
(send-text *conn* "Hello from the other side!")
(send-text *conn* "I must have called a thousand times...")
;; After the echo comes back, close it.
(close-connection *conn*)
(wait-until-closed *conn*)(defparameter *binance*
(connect-url "wss://stream.binance.com:9443/stream?streams=btcusdt@trade"
:handler #'handler))
;; The listener runs in the background, printing trades.
;; Later, shut it down:
(close-connection *binance*)(defparameter *conn* (connect "localhost" 8080 "/ws"
:background nil
:handler #'handler))
;; This blocks until the connection closes:
(run-message-loop *conn*)(defun my-handler (type data)
(case type
(:text (format t "Text: ~a~%" data))
(:binary (format t "Binary: ~a bytes~%" (length data)))
(:ping (format t "Received a ping from remote: ~a~%" data))
(:pong (format t "Received a pong from remote: ~a~%" data))
(:error (format t "Socket about to close due to error ~a~%" data))
(:close (format t "Closed with code ~d and reason ~a~%" (car data) (cdr data)))))- Writing from multiple threads is fine.
- You can safely call
close-connection,send-text, etc. from inside the handler. - Multiple concurrent calls to
close-connectionare harmless; only the first one takes effect.
BSD 2-clause.
The library is known to work on SBCL, CCL and ECL.
However, ECL’s execution/memory management model is ill‑suited to the library’s concurrent I/O performance; multiple streams or heavy loads are thus inadvisable on ECL.
