= WebSockets
Welcome
WebSockets provide a persistent, bidirectional connection between a client and a server. Once established, either party may send messages to the other at any time — making WebSockets the right choice for real-time features such as chat, live notifications, and collaborative editing.
If you only need a unidirectional stream of events from the server, consider Server-Sent Events instead.
What You Will Learn
After reading this guide, you will be able to:
-
Define a WebSocket route in Pedestal.
-
Handle WebSocket lifecycle events: open, message, and close.
-
Send text messages back to a connected client.
-
Connect to the WebSocket endpoint from a JavaScript client.
Guide Assumptions
This guide is for intermediate users who have worked through some sample applications or the first few Hello World guides. In particular, you should know how to run a build, start a REPL, and start and stop your server.
Getting Help if You’re Stuck
We’ll take this in small steps. If you get stuck at any point in this guide, please submit an issue about this guide or, hop over to the Pedestal Users mailing list and raise your hand there. You can also get help from the #pedestal channel on the Clojurians Slack.
WebSocket Overview
A WebSocket connection starts like any other HTTP request: the client sends a GET request
with a special Upgrade: websocket header.
If the server accepts, both sides switch to the WebSocket protocol on the same TCP connection.
From that point on, either party can send text or binary messages at any time until
one of them closes the connection.
Unlike SSE, where messages flow only from server to client, WebSockets are fully bidirectional.
Where We Are Going
We’ll create a simple echo endpoint at /ws/echo.
When a client sends a text message, the server echoes it back prefixed with "echo: ".
Setting Up
Create a new project directory:
$ mkdir websocket-demo $ cd websocket-demo $ mkdir -p src
{:paths ["src"]
:deps {io.pedestal/pedestal.http-kit {:mvn/version "0.8.2-beta-8"}
org.slf4j/slf4j-simple {:mvn/version "2.0.17"}}}
Starting Simple
Let’s start with the namespace and a basic home page to confirm the server is running before we add WebSocket support.
(ns websocket-demo
(:require [io.pedestal.connector :as conn]
[io.pedestal.http.http-kit :as hk]
[io.pedestal.service.websocket :as ws]))
(defn home-page
[_request]
{:status 200 :body "Hello, World!"})
Start the REPL, load the namespace, and verify the home page works:
$ clj Clojure 1.12 user=> (require 'websocket-demo) nil user=> (websocket-demo/start) ...
$ curl http://localhost:8890/ Hello, World!
Adding a WebSocket Route
WebSocket connections are handled by a websocket interceptor, created with
websocket-interceptor.
You provide callback functions for the WebSocket lifecycle events you care about.
(def echo-interceptor
(ws/websocket-interceptor
::echo
{:on-open (fn [_channel _request] (1)
(println "WebSocket connection opened")
nil)
:on-text (fn [channel _proc text] (2)
(ws/send-text! channel (str "echo: " text)))
:on-close (fn [_channel _proc reason] (3)
(println "WebSocket connection closed:" reason))}))
| 1 | :on-open is called when the connection is first established.
Its return value becomes the process object — passed as the second argument to all
subsequent callbacks.
For a simple echo endpoint there’s no shared state needed, so we return nil. |
| 2 | :on-text receives each text message from the client.
send-text! sends a reply back to the same client. |
| 3 | :on-close is called when the connection closes, regardless of which side initiated it.
The reason is a keyword such as :normal or :abnormal. |
|
There is also an |
Now add the route.
WebSocket upgrade requests are ordinary HTTP GET requests, so the route uses :get as
the method:
(def routes
#{["/" :get home-page :route-name ::home]
["/ws/echo" :get echo-interceptor]}) (1)
| 1 | WebSocket upgrade requests are always GET requests. |
And the connector plumbing (unchanged from the basic setup):
(defn create-connector []
(-> (conn/default-connector-map 8890)
(conn/with-default-interceptors)
(conn/with-routes routes)
(hk/create-connector nil)))
(defonce *connector (atom nil))
(defn start []
(reset! *connector (conn/start! (create-connector))))
(defn stop []
(conn/stop! @*connector)
(reset! *connector nil))
(defn restart []
(when @*connector (stop))
(start))
Trying It Out
Restart the connector to pick up the changes:
user=> (require :reload 'websocket-demo) nil user=> (websocket-demo/restart) ...
Install websocat if you don’t have it:
$ brew install websocat # macOS $ cargo install websocat # or via Rust
Then connect and send a message:
$ websocat ws://localhost:8890/ws/echo hello echo: hello goodbye echo: goodbye
Type a line and press Enter; the server echoes it back prefixed with "echo: ".
Press Ctrl-C to close the connection.
Connecting from JavaScript
To connect from a browser, use the standard WebSocket API:
const socket = new WebSocket("ws://localhost:8890/ws/echo");
socket.addEventListener("open", () => {
socket.send("hello from the browser");
});
socket.addEventListener("message", (event) => {
console.log("Received:", event.data);
});
socket.addEventListener("close", () => {
console.log("Connection closed");
});
Coverage of the JavaScript WebSocket API is beyond the scope of this guide. Consult the MDN documentation for a thorough introduction.
The Whole Shebang
For reference, here is the complete source:
{:paths ["src"]
:deps {io.pedestal/pedestal.http-kit {:mvn/version "0.8.2-beta-8"}
org.slf4j/slf4j-simple {:mvn/version "2.0.17"}}}
Wrapping Up
We’ve set up a minimal WebSocket endpoint, handled the open/message/close lifecycle,
and tested it with websocat and from JavaScript.
For more details — connection limits, binary messages, subprotocols, and the full API — see the WebSockets Reference.