{"title":"WebSockets and live pages","description":"Build a server-rendered Amber page that receives channel events through a local ES module","section":"guides","version":"v2","path":"guides/websockets","canonical_url":"https://amberframework.org/docs/v2/guides/websockets","markdown_url":"https://amberframework.org/docs/v2/guides/websockets.md","inherited":false,"content_markdown":"# WebSockets and live pages\n\nAmber's default remains server-rendered HTML. Add a WebSocket when the document\nis already useful and one part of it needs to change as work happens. Amber V2\nprovides client sockets, topic-based channels, broadcasts from controllers or\njobs, presence events, three decoders, and short-window connection recovery.\n\nThis guide builds one complete path. Every block names its destination.\n\n## 1. Generate the channel\n\n**Run from: the application root.**\n\n```bash\namber generate channel Status --topics=status\n```\n\n**File: `src/channels/status_channel.cr` — replace the generated\nhandler with this small rebroadcasting channel.**\n\n```crystal\nclass StatusChannel < Amber::WebSockets::Channel\n  def handle_message(client_socket, message)\n    rebroadcast!(message)\n  end\nend\n```\n\n`status:*` is a topic family. A page can join `status:reports`, while another\njoins `status:deploys`, without creating another channel class.\n\n## 2. Define the socket boundary\n\n**File: `src/sockets/user_socket.cr` — create this file.**\n\n```crystal\nstruct UserSocket < Amber::WebSockets::ClientSocket\n  channel \"status:*\", StatusChannel\n\n  def on_connect : Bool\n    true\n  end\nend\n```\n\nAuthentication belongs in `on_connect`. The socket exposes the request\n`session`, `cookies`, `params`, and `context`; return `false` to reject the\nconnection.\n\n**File: `src/my_app.cr` — require sockets and channels before the routes.**\n\n```crystal\nrequire \"./channels/**\"\nrequire \"./sockets/**\"\nrequire \"../config/routes\"\n```\n\nReplace `my_app` with the generated application filename.\n\n## 3. Register the handshake\n\n**File: `config/routes.cr` — add this line inside `routes :web`.**\n\n```crystal\nwebsocket \"/ws\", UserSocket\n```\n\n## 4. Join from a local ES module\n\n**File: `app/assets/javascript/live-status.js` — create this browser module.**\n\n```javascript\nconst protocol = location.protocol === \"https:\" ? \"wss\" : \"ws\";\nconst socket = new WebSocket(`${protocol}://${location.host}/ws`);\n\nsocket.addEventListener(\"open\", () => {\n  socket.send(JSON.stringify({\n    event: \"join\",\n    topic: \"status:reports\",\n    payload: {}\n  }));\n});\n\nsocket.addEventListener(\"message\", ({data}) => {\n  const message = JSON.parse(data);\n  if (message.event !== \"report:ready\") return;\n\n  document\n    .querySelector(`[data-report=\"${message.payload.id}\"]`)\n    ?.setAttribute(\"data-state\", \"ready\");\n});\n```\n\n**File: `src/views/layouts/application.ecr` — add the module to the existing\nimport map and import it after the map.**\n\n```ecr\n<%= javascript_importmap_tag(\n  {\n    \"app\" => \"javascript/app.js\",\n    \"live-status\" => \"javascript/live-status.js\"\n  },\n  preload: [\"javascript/app.js\", \"javascript/live-status.js\"]\n) %>\n<script type=\"module\">\n  import \"app\";\n  import \"live-status\";\n</script>\n```\n\nNo npm package, bundler, client framework, or CDN is required.\n\n## 5. Publish after work succeeds\n\n**File: the controller, service, or job that owns the successful operation.**\n\n```crystal\nStatusChannel.broadcast_to(\n  \"status:reports\",\n  \"report:ready\",\n  {\"id\" => report.id.to_s}\n)\n```\n\nBroadcast after the state change succeeds. A background job can call the same\nclass method when slow work finishes.\n\n## Protocol and lifecycle\n\nThe default JSON envelope contains `event`, `topic`, and `payload`. Clients send\n`join`, `message`, and `leave`; applications define their own event names for\nserver broadcasts. Amber also includes text and binary decoders, channel error\nisolation, presence join/leave diffs, a 30-second heartbeat, a 100-second idle\ntimeout, and a 60-second reconnection window with a bounded 100-message buffer.\n\nThose defaults are process-local. The built-in pub/sub adapter does not fan an\nevent across multiple Amber processes. Register a shared adapter before relying\non cross-instance broadcasts, and measure the proxy and operating-system limits\nfor the connection count your application expects.\n\n## Measured on the Amber website\n\nThe August 11, 2026 release candidate for this website uses the same channel\npath described above. On a DigitalOcean one-shared-vCPU, 512 MB-class target it\nheld 1,000 joined clients for 85 seconds with zero connection errors. While\nthose sockets remained open, a separate host drove the rendered `/index.json`\npath at a median 8,058 requests/second across three trials; the median trial's\np99 latency was 26.77 ms.\n\nThat is a dated boundary, not a universal connection limit. The clients were\nidle after joining, the test did not exercise fan-out, TLS, proxies, or multiple\nprocesses, and the sequential shared-vCPU stages were noisy. Read the\n[complete machine-readable evidence](/benchmarks/amber-v2-site-websocket-2026-08-11.json)\nbefore using the number for planning.\n\nContinue with [Sockets](sockets.md) for authentication and lifecycle hooks, or\n[Background jobs](../background-jobs/) to publish an event after queued work."}