Pulling Live Data into Salesforce with WebSockets and LWC

“The question isn't whether in-house or consulting is better. The question is which model helps your business move faster today.”

Most of the time, when a Salesforce component needs data from outside Salesforce, it asks a question and waits for an answer. It sends a request, gets a response, and that's the end of the conversation until it decides to ask again. This works fine for a lot of things. It falls apart the moment you need data that's always changing — a live call transcript, a stock ticker, a delivery location moving across a map, a chat message arriving.

We ran into exactly this while building a live call-transcript feature for Salesforce. Standard REST API calls don’t work for this use case because the words needed to appear on the agent's screen as they were spoken, not a few seconds later after a polling cycle. WebSockets turned out to be the right tool, and pairing them with a Lightning Web Component was surprisingly straightforward once we understood the pieces.

First, What Is a WebSocket?

To appreciate WebSockets, it helps to remember how normal web requests work.

A regular HTTP request is like sending a letter. You write it, mail it, and wait for a reply to come back. If you want to know whether anything has changed, you have to send another letter. And another. This is called polling, and it's wasteful. You're constantly asking "anything new? anything new? anything new?" and most of the time the answer is "nope."

A WebSocket is more like a phone call. You dial once, the line stays open, and then either side can talk whenever they have something to say. No hanging up and redialing. No asking over and over. The server can simply push new data to you the instant it exists, and you can send data back just as freely.

That open, two-way, always-on connection is the whole point. It's why WebSockets power live chats, multiplayer games, trading dashboards, and — in our case — real-time transcription.

A couple of quick facts worth knowing:

  • WebSocket connections use their own URL scheme: ws:// for a plain connection and wss:// for a secure, encrypted one. In Salesforce you'll always use wss://. Anything less won't be allowed.
  • Once the connection is open, data flows as messages. You listen for messages coming in, and you can send messages out, for as long as the connection lives.

Why This Matters Inside Salesforce

Here's the good news: a Lightning Web Component runs in the browser, and the browser already knows how to speak WebSocket. There's a built-in WebSocket object baked into every modern browser, so you don't need to install a library or pull in a special package. You just use it.

That means an LWC can open a direct line to an external system — a transcription service, a messaging server, whatever you've got — and receive live updates without ever refreshing the page or hammering an endpoint with repeated requests.

There's one setup step you can't skip, though, so let's get it out of the way first.

The One Piece of Setup: CSP Trusted Sites

Salesforce won't let your component connect to just any outside address. For security, you have to explicitly tell Salesforce implementation partner that a particular external endpoint is allowed. You do this through CSP Trusted Sites.

In Setup, search for CSP Trusted Sites, create a new entry, and:

  • Put your WebSocket server's address in the URL field (for example, wss://your-service.example.com).
  • Make sure the connect-src context is enabled. This is the specific permission that covers WebSocket connections.

If you skip this, your component will fail to connect and the browser console will show a Content Security Policy error. It's the single most common reason a WebSocket in LWC "just doesn't work," so it's worth checking first when something goes wrong.

Building the Component

Now the fun part. Let's build a small LWC that opens a WebSocket, listens for incoming messages, and displays them live — the same basic pattern we used for the call transcript.

The key idea is lifecycle. We open the connection when the component appears on screen, and — this part is important — we close it when the component goes away. Leaving connections open after a component is gone is a memory leak waiting to happen.

The JavaScript

import { LightningElement } from 'lwc';

 

export default class LiveTranscript extends LightningElement {

    // Holds the WebSocket connection so we can close it later

    socket;

 

    // The list of messages we've received, shown in the UI

    messages = [];

 

    // Simple status text for the agent: Connecting, Live, Disconnected...

    status = 'Connecting...';

 

    // connectedCallback runs when the component is inserted into the page.

    // This is the right place to open the connection.

    connectedCallback() {

        this.openSocket();

    }

 

    // disconnectedCallback runs when the component is removed.

    // Always close the connection here to avoid leaks.

    disconnectedCallback() {

        if (this.socket) {

            this.socket.close();

        }

    }

 

    openSocket() {

        // Use wss:// — Salesforce requires a secure connection.

        this.socket = new WebSocket('wss://your-service.example.com/transcript');

 

        // Fired once when the connection is successfully open.

        this.socket.onopen = () => {

            this.status = 'Live';

            // You can send a message to the server here if it expects one,

            // for example to identify which call's transcript you want.

            this.socket.send(JSON.stringify({ callId: '12345' }));

        };

 

        // Fired every time the server pushes a new message to us.

        this.socket.onmessage = (event) => {

            const data = JSON.parse(event.data);

            // Rebuild the array so LWC's reactivity picks up the change.

            this.messages = [...this.messages, data.text];

        };

 

        // Fired if something goes wrong with the connection.

        this.socket.onerror = () => {

            this.status = 'Connection error';

        };

 

        // Fired when the connection closes, for any reason.

        this.socket.onclose = () => {

            this.status = 'Disconnected';

        };

    }

}

 

Let's walk through what's happening, because the shape of this is the same for almost any WebSocket you'll ever build.

We create the connection with new WebSocket(...) and then attach four event handlers. onopen tells us the line is live, and it's a good spot to send an initial message — in our transcript case, we tell the server which call we care about. onmessage is the heart of it: every time the server has something new, this fires, and we add the incoming text to our list. onerror and onclose let us keep the agent informed instead of leaving them staring at a screen that silently stopped updating.

One small but important detail: notice we write this.messages = [...this.messages, data.text] rather than pushing onto the existing array. LWC only re-renders when it sees a property get reassigned. Mutating the array in place with .push() won't reliably update the screen, so we build a fresh array each time.

The HTML

The template is refreshingly simple. We show the status and loop over the messages as they arrive.

 

   

 

       

Status: {status}

 

 

       

           

{line}

 

       

 

   

 

 

 

 

As new messages land in the messages array, LWC automatically renders each one. There's no manual DOM manipulation and no refresh. The list simply grows on screen in real time, which is exactly the effect we wanted for a live transcript.

But Doesn't All This Live Data Mean a Ton of API Calls?

Nope — and that's one of the best things about WebSockets.

The usual way to get "live" data is polling: asking the server "anything new?" over and over. Once a second means 3,600 requests an hour per user, most coming back with nothing.

A WebSocket skips all that. Opening it takes one request. After that, every message flowing across the connection is not a new call — a thousand transcript updates is still just one open line. The server pushes data down the moment it exists.

There's a nice bonus for Salesforce too. Since the WebSocket runs in the browser, connecting the agent directly to the external service, that traffic never hits Salesforce's servers. So it doesn't touch your daily API limits at all.

The only catch: each open connection uses a bit of memory on the external server, so handling thousands at once has its own scaling to consider. But that's far lighter than firing off endless requests. For keeping API calls down, WebSockets win easily.

A Few Things We Learned the Hard Way

A working demo and a reliable production feature are two different things. A handful of lessons from actually shipping this:

Connections drop. Plan for it. Networks hiccup, servers restart, laptops go to sleep. A connection that was alive a minute ago can quietly die. For anything important, add reconnect logic in the onclose handler so the component tries to re-establish the line rather than just giving up. A short delay before retrying keeps you from hammering the server if it's briefly down.

Always clean up. We'll say it again because it bites people: close the socket in disconnectedCallback. An agent who opens and closes twenty records over a shift shouldn't accumulate twenty zombie connections.

Tell the user what's going on. That little status field earns its keep. "Live," "Reconnecting," "Disconnected" — these small signals mean the difference between a user who trusts the feature and one who wonders whether it's broken.

Check CSP Trusted Sites first when things fail. If your connection never opens and the console mentions a security policy, this is almost always the culprit. It's an easy fix once you know to look.

Wrapping Up

WebSockets sound intimidating until you realize the browser does most of the heavy lifting for you. Open a connection, listen for messages, update your component's data, and let LWC's reactivity paint the screen. That's genuinely most of it.

The pattern unlocks a whole category of features that polling can't do gracefully — anything where data is alive and changing and the user shouldn't have to wait or refresh to see it. For us that was a live call transcript streaming straight into Salesforce. For you it might be something else entirely. Either way, the phone line is open, and both sides can finally just talk.

Written by,
Sandip Panchani,

Salesforce Enterprise Architect