Documentation

WebSockets and live pages

Updated
Browse documentation

Published 2026-08-13. V2 is a prerelease; the web core is release-gated and other previews are named separately. What beta means.

Read this page as HTML, Markdown, or structured JSON—or open the published Markdown with an AI assistant. Gemini receives the prompt through your clipboard because its signed-out page does not reliably prefill URL text; paste when the new tab opens. External assistants need the public site URL.

WebSockets and live pages

Amber's default remains server-rendered HTML. Add a WebSocket when the document is already useful and one part of it needs to change as work happens. Amber V2 provides client sockets, topic-based channels, broadcasts from controllers or jobs, presence events, three decoders, and short-window connection recovery.

This guide builds one complete path. Every block names its destination.

1. Generate the channel

Run from: the application root.

Terminal
amber generate channel Status --topics=status

File: src/channels/status_channel.cr — replace the generated handler with this small rebroadcasting channel.

Crystal
class StatusChannel < Amber::WebSockets::Channel
  def handle_message(client_socket, message)
    rebroadcast!(message)
  end
end

status:* is a topic family. A page can join status:reports, while another joins status:deploys, without creating another channel class.

2. Define the socket boundary

File: src/sockets/user_socket.cr — create this file.

Crystal
struct UserSocket < Amber::WebSockets::ClientSocket
  channel "status:*", StatusChannel

  def on_connect : Bool
    true
  end
end

Authentication belongs in on_connect. The socket exposes the request session, cookies, params, and context; return false to reject the connection.

File: src/my_app.cr — require sockets and channels before the routes.

Crystal
require "./channels/**"
require "./sockets/**"
require "../config/routes"

Replace my_app with the generated application filename.

3. Register the handshake

File: config/routes.cr — add this line inside routes :web.

Crystal
websocket "/ws", UserSocket

4. Join from a local ES module

File: app/assets/javascript/live-status.js — create this browser module.

JavaScript
const protocol = location.protocol === "https:" ? "wss" : "ws";
const socket = new WebSocket(`${protocol}://${location.host}/ws`);

socket.addEventListener("open", () => {
  socket.send(JSON.stringify({
    event: "join",
    topic: "status:reports",
    payload: {}
  }));
});

socket.addEventListener("message", ({data}) => {
  const message = JSON.parse(data);
  if (message.event !== "report:ready") return;

  document
    .querySelector(`[data-report="${message.payload.id}"]`)
    ?.setAttribute("data-state", "ready");
});

File: src/views/layouts/application.ecr — add the module to the existing import map and import it after the map.

ECR template
<%= javascript_importmap_tag(
  {
    "app" => "javascript/app.js",
    "live-status" => "javascript/live-status.js"
  },
  preload: ["javascript/app.js", "javascript/live-status.js"]
) %>
<script type="module">
  import "app";
  import "live-status";
</script>

No npm package, bundler, client framework, or CDN is required.

5. Publish after work succeeds

File: the controller, service, or job that owns the successful operation.

Crystal
StatusChannel.broadcast_to(
  "status:reports",
  "report:ready",
  {"id" => report.id.to_s}
)

Broadcast after the state change succeeds. A background job can call the same class method when slow work finishes.

Protocol and lifecycle

The default JSON envelope contains event, topic, and payload. Clients send join, message, and leave; applications define their own event names for server broadcasts. Amber also includes text and binary decoders, channel error isolation, presence join/leave diffs, a 30-second heartbeat, a 100-second idle timeout, and a 60-second reconnection window with a bounded 100-message buffer.

Those defaults are process-local. The built-in pub/sub adapter does not fan an event across multiple Amber processes. Register a shared adapter before relying on cross-instance broadcasts, and measure the proxy and operating-system limits for the connection count your application expects.

Measured on the Amber website

The August 11, 2026 release candidate for this website uses the same channel path described above. On a DigitalOcean one-shared-vCPU, 512 MB-class target it held 1,000 joined clients for 85 seconds with zero connection errors. While those sockets remained open, a separate host drove the rendered /index.json path at a median 8,058 requests/second across three trials; the median trial's p99 latency was 26.77 ms.

That is a dated boundary, not a universal connection limit. The clients were idle after joining, the test did not exercise fan-out, TLS, proxies, or multiple processes, and the sequential shared-vCPU stages were noisy. Read the complete machine-readable evidence before using the number for planning.

Continue with Sockets for authentication and lifecycle hooks, or Background jobs to publish an event after queued work.