Real-Time Communication with WebSockets
WebSockets — Live, Two-Way Connections Without the Request Overhead
HTTP is a request–response protocol: the client sends a request, the server replies, and the connection closes. That works well for loading pages and calling APIs, but it is the wrong tool for anything that must push data from the server to the client without being asked — live chat messages, real-time notifications, live dashboards.
WebSockets solve this by upgrading an HTTP connection to a persistent, full-duplex channel. Once the handshake completes, both sides can send frames at any time without the overhead of a new HTTP request for every message.
Why does this matter for ColdFusion developers?
- ColdFusion 2025 ships with a built-in WebSocket server — no extra daemon, no third-party proxy
- The HTTP server runs on port 8500 in the lab, but the WebSocket server runs on a separate port — 8585 by default
- The server side is a plain CFC that extends
CFIDE.websocket.ChannelListener— the same CFC model you already know - You can push a message to every connected client from any CFML page with a single function call:
wsPublish - The browser connects using the
<cfwebsocket>CFML tag — not a rawnew WebSocket()call
💡 Two ports — HTTP on 8500, WebSocket on 8585
This is one of the most common sources of confusion in CF WebSocket setup. The HTTP server (where you open .cfm pages) runs on port 8500. The WebSocket server runs on a different port — 8585 by default. You can confirm the WebSocket port at any time:
curl -s http://localhost:8500/CFIDE/administrator/index.cfm | grep websocket_port
![[object Object]](/content/files/courses/ColdFusion-2025-Foundations-5151cba6/module-3/5.lesson-websockets/__static__/cf-websocket-architecture-v1.png?v=1789774489337)
ColdFusion's built-in WebSocket server: register channels in Application.cfc, implement a handler CFC, connect from the browser — all on the same port.
1. The WebSocket handler CFC
The server side of a WebSocket channel is a CFC with three lifecycle methods. ColdFusion calls them automatically as connections open, receive messages, and close.

Three lifecycle methods: onWSOpen when a client connects, onWSMessage when a frame arrives, onWSClose when the connection drops.
// WSHandler.cfc — place in the CF wwwroot
component extends="CFIDE.websocket.ChannelListener" {
/**
* Called when a message arrives on this channel.
* channel — name of the channel the message was sent to
* data — the raw message string (often JSON)
* client — struct with clientid, subscribed channels, etc.
*/
public void function onWSMessage(
required string channel,
required any data,
required struct client
) {
// Echo the message back to every subscriber on the same channel
wsPublish(channel, data);
}
/**
* Called when a new client subscribes to any channel
* handled by this CFC.
*/
public void function onWSOpen(required struct client) {
writeLog(
file = "websocket",
text = "WS connection opened: #client.clientid#"
);
}
/**
* Called when a client disconnects (tab closed, network drop, etc.).
*/
public void function onWSClose(required struct client) {
writeLog(
file = "websocket",
text = "WS connection closed: #client.clientid#"
);
}
}
The client struct is provided by ColdFusion and contains at minimum clientid — a unique identifier for that connection. You can use it to send targeted messages or track online users.
⚠️ Your handler CFC must extend CFIDE.websocket.ChannelListener
This is non-negotiable and not obvious from the ColdFusion documentation. ColdFusion validates the handler CFC at startup by calling isInstanceOf("CFIDE.websocket.ChannelListener") on it. If the component does not extend that base, the entire application throws a 500 error on every request.
The base CFC already exists at /opt/coldfusion2025/cfusion/wwwroot/CFIDE/websocket/ChannelListener.cfc and provides default pass-through implementations of all six listener methods (allowSubscribe, allowPublish, beforePublish, canSendMessage, beforeSendMessage, afterUnsubscribe). Your handler only needs to override the ones it cares about.
// ✗ Wrong — CF throws InvalidListenerException at startup
component {
public void function onWSMessage(...) { ... }
}
// ✓ Correct
component extends="CFIDE.websocket.ChannelListener" {
public void function onWSMessage(...) { ... }
}
If you edit WSHandler.cfc and the error persists after restarting CF, delete the compiled class cache — CF may be loading a stale compiled version:
rm -f /opt/coldfusion2025/cfusion/wwwroot/WEB-INF/cfclasses/cfWSHandler* && \
sudo /opt/coldfusion2025/cfusion/bin/coldfusion restart
📖 What else is in the client struct?
ColdFusion populates the client struct with metadata about the WebSocket connection. The most useful keys:
| Key | Type | Description |
|---|---|---|
clientid | string | Unique ID for this connection — generated by CF |
subscriptions | array | List of channel names this client is subscribed to |
cfid | string | ColdFusion session ID, if the user has an active session |
cftoken | string | ColdFusion session token, if applicable |
You can use clientid with wsSendMessage(clientid, data) to push a message to one specific client instead of broadcasting to everyone. This is useful for targeted notifications (e.g., "Your export is ready") where you do not want to broadcast to every connected user.
2. Register channels in Application.cfc
Channels must be declared in Application.cfc before the WebSocket server will accept connections to them. Use the this.wschannels array — each entry is a struct with a name and a cfclistener:

Both channels point to the same handler CFC. Multiple channels can share a handler — they are distinguished by the channel argument in onWSMessage.
// Application.cfc
component {
this.name = "MyApp";
// Declare WebSocket channels.
// Each channel needs: name (string) and cfclistener (CFC name).
this.wschannels = [
{ name="chat", cfclistener="WSHandler" },
{ name="notifications", cfclistener="WSHandler" }
];
}
A few rules:
- The
cfclistenervalue is the CFC name without.cfc, resolved relative to the webroot (or via the component path) - The handler CFC must be in the same directory or a subdirectory of the application — CF will not find it otherwise
- Channel names must be unique across the application
- Use
name="channelName"syntax (equals sign, not colon) — the JSON colon syntax{"name": "chat"}is not supported inthis.wschannels - You must restart the CF application (or touch
Application.cfc) after changingthis.wschannels - If no
cfclisteneris specified, ColdFusion uses the defaultCFIDE/websocket/ChannelListener.cfcwhich allows everything through
⚠️ Use name= syntax, not JSON colon syntax
ColdFusion struct literals in this.wschannels must use the equals sign syntax, not quoted JSON-style keys:
// ✓ Correct
this.wschannels = [{ name="chat", cfclistener="WSHandler" }];
// ✗ Wrong — CF silently misreads this
this.wschannels = [{ "name": "chat", "cfclistener": "WSHandler" }];
3. The browser client
ColdFusion provides the <cfwebsocket> tag to create WebSocket connections from a CFM page. The tag handles the subscription handshake automatically and wraps the connection in a named JavaScript object you can call from your own code:
<!-- Creates a JavaScript object named "ws" subscribed to the chat channel -->
<cfwebsocket
name = "ws"
onMessage = "handleMessage"
onOpen = "handleOpen"
onClose = "handleClose"
subscribeTo = "chat"
>
<script>
function handleOpen() {
document.getElementById("status").textContent = "Connected";
}
function handleMessage(msg) {
// msg.data contains the payload; msg.type is "data" for real messages
if (msg.type === "data") {
document.getElementById("chat-log").insertAdjacentHTML(
"beforeend",
`<p><strong>${msg.publisherID}</strong>: ${msg.data}</p>`
);
}
}
function handleClose() {
document.getElementById("status").textContent = "Disconnected";
}
// Publish a message to the chat channel
function sendMessage(text) {
ws.publish("chat", text);
}
</script>
The <cfwebsocket> tag sends a CF-specific subscription handshake after the TCP connection opens — this is what registers the client with wsGetSubscribers. A plain new WebSocket() call opens the TCP socket but skips the handshake, so CF never sees the client as subscribed.
📖 cfwebsocket tag attributes
| Attribute | Required | Description |
|---|---|---|
name | Yes | Name of the JavaScript object created in the page. Use it to call .publish(), .subscribe(), .unsubscribe(), etc. |
onMessage | Yes | JavaScript function called every time the server sends a frame |
onOpen | No | JavaScript function called when the connection is established |
onClose | No | JavaScript function called when the connection drops |
onError | No | JavaScript function called on error — receives codes -1 (channel error) and 4001 (application error) |
subscribeTo | No | Comma-separated list of channels to subscribe to automatically on connect |
useCFAuth | No | If true (default), uses the ColdFusion session for authentication — no separate login needed |
The JavaScript object created by name exposes these methods: .publish(channel, message), .subscribe(channel), .unsubscribe(channel), .getSubscriberCount(channel), .isConnectionOpen().
📖 What does the message object look like in onMessage?
Every message your onMessage function receives is a JavaScript object with these keys:
| Key | Description |
|---|---|
type | "data" for real messages, "response" for system acknowledgements (subscribe, unsubscribe, etc.) |
code | 0 = success, -1 = channel error, 4001 = application error |
reqType | The request type: "subscribe", "publish", "data", etc. |
data | The message payload — present when type is "data" |
clientid | Unique ID of the connected client |
publisherID | Client ID of who published. 0 means it came from a server-side wsPublish call |
channelname | The channel the message arrived on |
msg | Human-readable status — "ok" on success, error description on failure |
Always check msg.type === "data" before rendering — system responses (type: "response") will also arrive in your onMessage handler.
💡 Why ws:// and not wss://?
ws:// is the unencrypted WebSocket protocol, analogous to http://. wss:// is the TLS-encrypted equivalent, analogous to https://. In the lab the CF server runs without TLS, so ws:// is correct. In production, traffic should always use wss:// — configure TLS on CF or terminate it at a load balancer/reverse proxy and forward as ws:// internally.
To enable a secure wss:// connection you need a TLS certificate issued by a Certificate Authority (CA) — either a trusted public CA (such as Let's Encrypt) or your organisation's internal CA.
4. Push messages from server-side CFML
wsPublish broadcasts a message to every client currently subscribed to a channel. You can call it from any CFML page, scheduled task, or CFC — not just from inside the handler:
<cfscript>
// Broadcast a notification to all connected users
wsPublish("notifications", serializeJSON({
type: "alert",
message: "New ticket assigned to you",
timestamp: dateTimeFormat(now(), "yyyy-mm-dd HH:nn:ss")
}));
</cfscript>
This is the pattern for server-initiated pushes: a background job detects an event (new ticket, completed export, price change) and calls wsPublish — all connected clients receive the update without polling.
| Function | Signature | What it does |
|---|---|---|
wsPublish | wsPublish(channel, message) | Broadcast to all subscribers on a channel |
wsSendMessage | wsSendMessage(clientid, message) | Send to one specific connected client |
wsGetAllChannels | wsGetAllChannels() | Returns array of all registered channel names |
wsGetSubscribers | wsGetSubscribers(channel) | Returns array of client structs subscribed to a channel |
5. Common use cases

Four common WebSocket patterns — all supported natively with ColdFusion's built-in WS server and wsPublish.
| Use case | Channel strategy | Pattern |
|---|---|---|
| Live chat | Single chat channel | onWSMessage calls wsPublish(channel, data) — every subscriber gets every message |
| Push notifications | Single notifications channel | Server-side CFML calls wsPublish when an event fires |
| Live dashboard | dashboard channel | Scheduled task or loop calls wsPublish every N seconds with fresh metrics |
| Collaborative editing | doc-{id} per document | Dynamic channel per resource; use wsSendMessage for targeted routing |
Activity 1 — Create the WebSocket handler CFC
Create WSHandler.cfc in the ColdFusion webroot:
cat > /opt/coldfusion2025/cfusion/wwwroot/WSHandler.cfc << 'EOF'
component extends="CFIDE.websocket.ChannelListener" {
public void function onWSMessage(
required string channel,
required any data,
required struct client
) {
wsPublish(channel, data);
}
public void function onWSOpen(required struct client) {
writeLog(file="websocket", text="WS opened: #client.clientid#");
}
public void function onWSClose(required struct client) {
writeLog(file="websocket", text="WS closed: #client.clientid#");
}
}
EOF
Verify the file was created:
grep -l "wsPublish\|onWSMessage" /opt/coldfusion2025/cfusion/wwwroot/*.cfc
Activity 2 — Create ws_demo.cfm and register the channel
Step 1 — Create Application.cfc in the webroot with the chat channel registered:
cat > /opt/coldfusion2025/cfusion/wwwroot/Application.cfc << 'EOF'
component {
this.name = "MyApp";
this.wschannels = [
{ name="chat", cfclistener="WSHandler" }
];
}
EOF
Verify it was created correctly:
grep -A3 "wschannels" /opt/coldfusion2025/cfusion/wwwroot/Application.cfc
Step 2 — Create ws_demo.cfm with a chat UI using the <cfwebsocket> tag:
cat > /opt/coldfusion2025/cfusion/wwwroot/ws_demo.cfm << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CF WebSocket Demo</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 2rem auto; }
#chat-log { border: 1px solid #ccc; height: 200px; overflow-y: auto; padding: 0.5rem; margin-bottom: 0.5rem; }
#msg-input { width: 75%; padding: 0.4rem; }
button { padding: 0.4rem 1rem; }
</style>
</head>
<body>
<h2>WebSocket Chat Demo</h2>
<div id="chat-log"></div>
<input id="msg-input" type="text" placeholder="Type a message…">
<button onclick="sendMsg()">Send</button>
<p id="status">Connecting…</p>
<cfwebsocket
name = "chatWS"
onMessage = "handleMessage"
onOpen = "handleOpen"
onClose = "handleClose"
subscribeTo = "chat"
>
<script>
const log = document.getElementById("chat-log");
const status = document.getElementById("status");
function handleOpen() {
status.textContent = "Connected";
}
function handleMessage(msg) {
if (msg.type === "data") {
log.insertAdjacentHTML("beforeend",
`<p><strong>user</strong>: ${msg.data}</p>`);
log.scrollTop = log.scrollHeight;
}
}
function handleClose() {
status.textContent = "Disconnected";
}
function sendMsg() {
const input = document.getElementById("msg-input");
if (!input.value.trim()) return;
chatWS.publish("chat", input.value);
input.value = "";
}
</script>
</body>
</html>
EOF
Step 3 — Verify the page returns HTTP 200:
curl -s -o /dev/null -w "%{http_code}" http://localhost:8500/ws_demo.cfm
You should see 200. Open http://localhost:8500/ws_demo.cfm in the lab browser to see the chat UI:

The chat demo page — once the WebSocket handshake completes the status line changes from "Connecting…" to "Connected".
⚠️ Page returns 500 or blank?
The most common cause is a syntax error in Application.cfc. Check that the this.wschannels line is inside the component { } block and that all curly braces are balanced. You can also hit http://localhost:8500/Application.cfc in the CF admin browser tab to trigger a parse error message.
⚠️ Status shows "Connecting…" or immediately "Disconnected"?
This means the WebSocket channel failed to initialise — the browser connected but CF rejected the subscription. The most likely cause is that WSHandler.cfc is missing extends="CFIDE.websocket.ChannelListener" or there is a stale compiled class from a previous version.
Run these commands to fix it:
# 1. Recreate WSHandler.cfc with the correct extends
cat > /opt/coldfusion2025/cfusion/wwwroot/WSHandler.cfc << 'EOF'
component extends="CFIDE.websocket.ChannelListener" {
public void function onWSMessage(
required string channel,
required any data,
required struct client
) {
wsPublish(channel, data);
}
public void function onWSOpen(required struct client) {
writeLog(file="websocket", text="WS opened: #client.clientid#");
}
public void function onWSClose(required struct client) {
writeLog(file="websocket", text="WS closed: #client.clientid#");
}
}
EOF
# 2. Clear the compiled class cache
rm -f /opt/coldfusion2025/cfusion/wwwroot/WEB-INF/cfclasses/cfWSHandler*
# 3. Restart ColdFusion
sudo /opt/coldfusion2025/cfusion/bin/coldfusion restart
Wait ~20 seconds for CF to come back up, then refresh ws_demo.cfm — the status should change to Connected.
Activity 3 — Test end-to-end with a server push
ColdFusion's WebSocket subscription protocol requires a specific handshake message that only the <cfwebsocket> tag sends automatically. Command-line tools like websocat open a raw TCP connection but never send this handshake, so CF never registers them as subscribers. The correct end-to-end test uses the browser and a server-side push.
This activity needs two terminal tabs open at the same time. Click the + button at the top of the terminal panel to open a second tab:

Click + to open a second terminal tab. Click each tab to switch between them.
Step 1 — In Terminal 1, create the server-side push script:
cat > /opt/coldfusion2025/cfusion/wwwroot/ws_push_test.cfm << 'EOF'
<cfscript>
wsPublish("chat", "Hello from wsPublish — " & timeFormat(now(), "HH:mm:ss"));
writeOutput("Published");
</cfscript>
EOF
Step 2 — Open http://localhost:8500/ws_demo.cfm in the lab browser. Wait until the status line shows Connected.
Step 3 — In Terminal 2, trigger the server-side push:
curl -s http://localhost:8500/ws_push_test.cfm
You should see Published in Terminal 2 and the message appear in the chat log in the browser instantly.
⚠️ Browser shows "Connected" but no message appears after the push?
Check that ws_push_test.cfm returned Published (not a CF error). If it did but the message still didn't appear, open the browser DevTools console — look for WebSocket errors or check that the handleMessage function is correctly wired to the <cfwebsocket> tag's onMessage attribute.
Troubleshooting WebSockets in ColdFusion
Real-world WebSocket setup surfaces a few gotchas that are not obvious from the documentation. Here is what to check when things do not work.
Raw new WebSocket() connects but wsGetSubscribers returns 0
ColdFusion WebSockets use a proprietary subscription handshake on top of the standard WebSocket protocol. A plain new WebSocket() in JavaScript (or tools like websocat) only opens the TCP connection — it never sends the CF subscription message, so the server never registers the client as a subscriber and wsPublish delivers nothing.
Always use the <cfwebsocket> tag in your CFM pages. The tag sends the subscription handshake automatically and wraps the connection in a named JavaScript object (chatWS.publish(), chatWS.subscribe(), etc.):
<cfwebsocket name="chatWS" onMessage="handleMessage" subscribeTo="chat">
WSHandler is not a valid ChannelListener in the exception log
The handler CFC must extend CFIDE.websocket.ChannelListener, not just declare the methods bare. ColdFusion calls isInstanceOf("CFIDE.websocket.ChannelListener") on the CFC at startup — if the component does not extend that base, the channel fails to initialise and every request to the app returns 500.
// ✗ Wrong — CF rejects this
component {
public void function onWSMessage(...) { ... }
}
// ✓ Correct
component extends="CFIDE.websocket.ChannelListener" {
public void function onWSMessage(...) { ... }
}
The base CFC lives at /opt/coldfusion2025/cfusion/wwwroot/CFIDE/websocket/ChannelListener.cfc and already provides default implementations of allowSubscribe, allowPublish, beforePublish, canSendMessage, beforeSendMessage, and afterUnsubscribe — your handler only needs to override the methods it cares about.
WebSocket port is not 8500
The CF HTTP server runs on port 8500, but the WebSocket server runs on a separate port — 8585 by default. Always connect websocat and browser clients to port 8585:
# ✗ Wrong port
websocat ws://localhost:8500/cfusion/WS/chat
# ✓ Correct
websocat ws://localhost:8585/cfusion/WS/chat
You can confirm the WebSocket port at any time:
curl -s http://localhost:8500/CFIDE/administrator/index.cfm | grep websocket_port
Browser WebSocket connects to the wrong host
If ws_demo.cfm is accessed via an IP address (e.g. 172.16.0.2) but the JavaScript hardcodes ws://localhost:8585, the browser will try to connect to its own localhost — not the server. Always use ColdFusion's cgi scope to build the URL dynamically:
// ColdFusion evaluates this server-side before sending HTML to the browser
const ws = new WebSocket("ws://#cgi.server_name#:8585/cfusion/WS/chat");
Channel initialisation error on every request
If application.log shows Channel chat Initialization Exception repeating every second, it means CF is retrying the channel setup on every request because it keeps failing. Fix WSHandler.cfc (add extends="CFIDE.websocket.ChannelListener"), then do a full CF restart — a touch Application.cfc reload is not enough once the channel is in a failed state:
sudo /opt/coldfusion2025/cfusion/bin/coldfusion restart
Edited the CFC but the error persists after restart
ColdFusion caches compiled CFC classes in wwwroot/WEB-INF/cfclasses/. If you edit WSHandler.cfc but the error keeps happening, CF may have reused the old cached .class file instead of recompiling. Delete the cache and restart:
rm -f /opt/coldfusion2025/cfusion/wwwroot/WEB-INF/cfclasses/cfWSHandler* && \
sudo /opt/coldfusion2025/cfusion/bin/coldfusion restart
This forces CF to recompile WSHandler.cfc from source on the next request.
Understanding WebSocket response codes
Every server response includes a code field. Use it in your ws.onmessage handler to detect errors:
| Code | Category | Meaning |
|---|---|---|
0 | Success | Request completed successfully |
-1 | Channel error | Channel not found, or client already subscribed |
4001 | Application error | Runtime error while invoking a CFC method |
If you have defined a ws.onerror handler it receives codes -1 and 4001. If not, they arrive in ws.onmessage — check event.data.code to distinguish errors from normal messages:
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.code === -1) { console.error("Channel error:", msg.msg); return; }
if (msg.code === 4001) { console.error("Application error:", msg.msg); return; }
// normal message — msg.data contains the payload
};
Key takeaways
| Concept | ColdFusion approach |
|---|---|
| Declare a channel | this.wschannels = [{name="chat", cfclistener="WSHandler"}] in Application.cfc |
| Handler CFC | Must extends="CFIDE.websocket.ChannelListener" — bare component {} is rejected |
| Channel struct syntax | Use name="chat" (equals), not "name": "chat" (colon) |
| Browser client | Use <cfwebsocket> tag — not raw new WebSocket() |
| Handle incoming messages | onWSMessage(channel, data, client) in the handler CFC |
| Broadcast to all subscribers | wsPublish(channelName, message) |
| Send to one client | wsSendMessage(client.clientid, message) |
| WebSocket port | Port 8585 (not 8500) — confirm with grep websocket_port in CF admin page |
| Server-initiated push | Call wsPublish from any CFML page, scheduled task, or CFC |
| Stale class cache | Delete WEB-INF/cfclasses/cfWSHandler* and restart CF if edits don't take effect |
When all the checks above are green, this lesson is complete. Your progress is saved automatically — move straight on to the next lesson.
Found a bug or an issue with this lesson? Please reach out — your feedback helps improve the course for everyone.
📧 Alex — mercadoalexatgmail.com
Further reading
- Previous lesson
- Testing & Debugging CFML
- Next lesson
- Integration via Web Services (SOAP)