# How to Implement Real-Time Updates in Retool

- Tool: Retool
- Difficulty: Advanced
- Time required: 30-40 min
- Compatibility: Retool Cloud and Self-hosted
- Last updated: March 2026

## TL;DR

Retool doesn't have a native WebSocket component, but you can open a WebSocket connection inside a JS Query using the native WebSocket API. Store the socket instance in a Temporary State variable, listen for messages with onmessage, and call await stateVar.setValue(newData) to update the UI. For most dashboards, query polling (query settings → Polling interval) is simpler and sufficient.

## WebSockets vs Polling: Choosing the Right Real-Time Strategy

Retool's default data model is pull-based: queries run when triggered and return a snapshot of data. For most internal tools this is sufficient — refreshing every 30-60 seconds is fine for operations dashboards. But some use cases genuinely need push-based updates: live order feeds, real-time chat, IoT sensor streams, or trading dashboards.

Retool supports real-time updates through two mechanisms: (1) query polling intervals — simple, built-in, no custom code — and (2) WebSocket connections inside JS Queries — more complex but enables true server-push.

This tutorial covers both approaches and helps you decide which to use.

## Before you start

- A Retool app with at least one Table or Text component to display live data
- A WebSocket endpoint for real-time data (or use a public test endpoint like wss://echo.websocket.org for testing)
- Familiarity with JavaScript async/await and the native WebSocket API
- Understanding of Retool Temporary State variables and setValue()

## Step-by-step guide

### 1. Set up query polling for simple real-time refreshes

The easiest real-time approach is query polling. Open any query and go to its Advanced settings. Find 'Polling interval' (some Retool versions call it 'Auto-run every X seconds'). Set it to 10, 30, or 60 seconds. Retool will automatically re-run the query at that interval and update all bound components. This is suitable for dashboards, status monitors, and alert feeds where sub-second freshness is not required. The downside: every poll hits your database, regardless of whether data changed.

**Expected result:** Query runs automatically at the configured interval without user interaction.

### 2. Create Temporary State variables for WebSocket state

Before writing the WebSocket JS Query, create two Temporary State variables in the State panel (click + near the State section): (1) 'wsMessages' — type Array, default value [] — to hold incoming messages. (2) 'wsStatus' — type String, default value 'disconnected' — to track connection state. These variables will be updated from inside the JS Query's WebSocket callbacks, making their values visible to other components via {{ wsMessages.value }} and {{ wsStatus.value }}.

**Expected result:** Two Temporary State variables (wsMessages, wsStatus) appear in the State panel.

### 3. Write the WebSocket JS Query

Create a new JS Query named 'connectWebSocket'. Write the code to open a WebSocket connection. Store the socket object in a module-level variable so it persists across the query's lifecycle. In the onmessage callback, parse the incoming data and call await wsMessages.setValue() to update the UI. The critical gotcha: setValue() is async — after calling it, the updated value is not immediately readable in the same synchronous call. Build the new array from the current state value before updating.

```
// JS Query: connectWebSocket
// Set to 'Run on page load'

const WS_URL = 'wss://your-realtime-endpoint.com/feed';

const socket = new WebSocket(WS_URL);

socket.onopen = async () => {
  await wsStatus.setValue('connected');
  // Send auth if needed:
  // socket.send(JSON.stringify({ type: 'auth', token: 'Bearer xyz' }));
};

socket.onmessage = async (event) => {
  let parsed;
  try {
    parsed = JSON.parse(event.data);
  } catch {
    parsed = { raw: event.data };
  }

  // Get current messages and prepend new one
  // Note: do NOT read wsMessages.value immediately after setValue —
  // setValue is async and the value won't reflect the update yet.
  const current = wsMessages.value || [];
  const updated = [parsed, ...current].slice(0, 100); // Keep last 100
  await wsMessages.setValue(updated);
};

socket.onerror = async (err) => {
  await wsStatus.setValue('error');
  utils.showNotification({
    title: 'WebSocket error',
    description: 'Connection failed. Check your endpoint URL.',
    notificationType: 'error',
  });
};

socket.onclose = async () => {
  await wsStatus.setValue('disconnected');
};
```

**Expected result:** Query opens a WebSocket connection. Incoming messages update wsMessages.value and appear in bound components.

### 4. Display live messages in a Table or List component

Bind a Table component's Data source to {{ wsMessages.value }}. The table will update automatically whenever wsMessages.setValue() is called from the WebSocket callback. Add a Text component showing the connection status: 'Status: {{ wsStatus.value }}' with conditional color formatting (green for 'connected', red for 'error'). You can also bind individual fields: {{ wsMessages.value[0].price }} to show the most recent price tick.

**Expected result:** Table displays incoming WebSocket messages in real time as they arrive.

### 5. Implement reconnection logic

WebSocket connections drop due to network issues, server restarts, or idle timeouts. Add reconnection logic to the onclose handler. Use a JS-level timeout to attempt reconnection after a delay. Implement exponential backoff to avoid hammering the server. Track reconnection attempts in a Temporary State variable.

```
// Enhanced connectWebSocket with reconnection
let reconnectAttempts = 0;
const MAX_RECONNECTS = 5;

const connect = async () => {
  const socket = new WebSocket('wss://your-endpoint.com/feed');

  socket.onopen = async () => {
    reconnectAttempts = 0;
    await wsStatus.setValue('connected');
  };

  socket.onclose = async () => {
    await wsStatus.setValue('disconnected');
    if (reconnectAttempts < MAX_RECONNECTS) {
      reconnectAttempts++;
      const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
      await wsStatus.setValue(`reconnecting (${reconnectAttempts}/${MAX_RECONNECTS})...`);
      setTimeout(connect, delay);
    }
  };

  socket.onmessage = async (event) => {
    const current = wsMessages.value || [];
    await wsMessages.setValue([JSON.parse(event.data), ...current].slice(0, 100));
  };
};

await connect();
```

**Expected result:** Connection drops are handled automatically with exponential backoff reconnection.

## Complete code example

File: `JS Query: connectWebSocket (full implementation)`

```javascript
// JS Query: connectWebSocket
// Full WebSocket implementation with auth, heartbeat, and reconnection
// Set this query to 'Run on page load'

const WS_URL = retoolContext.configVars.WS_ENDPOINT || 'wss://echo.websocket.org';
const AUTH_TOKEN = retoolContext.configVars.WS_AUTH_TOKEN;

let socket = null;
let heartbeatTimer = null;
let reconnectAttempts = 0;
const MAX_RECONNECTS = 5;

const clearHeartbeat = () => {
  if (heartbeatTimer) clearInterval(heartbeatTimer);
};

const startHeartbeat = (ws) => {
  heartbeatTimer = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({ type: 'ping' }));
    }
  }, 30000);
};

const connect = async () => {
  socket = new WebSocket(WS_URL);

  socket.onopen = async () => {
    reconnectAttempts = 0;
    await wsStatus.setValue('connected');
    if (AUTH_TOKEN) {
      socket.send(JSON.stringify({ type: 'auth', token: AUTH_TOKEN }));
    }
    startHeartbeat(socket);
  };

  socket.onmessage = async (event) => {
    if (event.data === 'pong') return; // Ignore heartbeat responses
    try {
      const parsed = JSON.parse(event.data);
      const current = wsMessages.value || [];
      // Note: setValue() is async — the value is NOT immediately readable
      // after this call in the same synchronous context.
      await wsMessages.setValue([parsed, ...current].slice(0, 200));
    } catch {
      console.log('Non-JSON message:', event.data);
    }
  };

  socket.onerror = async () => {
    await wsStatus.setValue('error');
  };

  socket.onclose = async () => {
    clearHeartbeat();
    await wsStatus.setValue('disconnected');
    if (reconnectAttempts < MAX_RECONNECTS) {
      reconnectAttempts++;
      const delay = Math.min(1000 * 2 ** reconnectAttempts, 30000);
      setTimeout(connect, delay);
    } else {
      await wsStatus.setValue('failed — reload the page');
    }
  };
};

await connect();
```

## Common mistakes

- **Calling await wsMessages.setValue(newData) then immediately reading wsMessages.value expecting the new data** — undefined Fix: setValue() is async — the Temporary State value does not update synchronously. Build the new array from the current value before calling setValue(), do not read back after.
- **Creating a new WebSocket connection every time the query runs without closing the previous one** — undefined Fix: Check if a socket already exists and is open before creating a new one. Store the socket in a window-level variable or use a Temporary State variable to track connection state.
- **Forgetting that JS Queries in Retool run in a sandboxed environment without persistent global scope** — undefined Fix: Variables defined inside a JS Query are lost when the query completes. Use Temporary State variables via setValue() to persist data between query executions.

## Best practices

- Use query polling (30-60 second intervals) for dashboards where sub-second freshness is not required — it's simpler and more reliable
- Limit the in-memory message buffer (e.g., last 200 messages) to prevent memory growth in long-running sessions
- Always implement reconnection logic — WebSocket connections drop for many reasons in real-world environments
- Use Retool configVars for WebSocket endpoint URLs and auth tokens — never hardcode credentials in query code
- Combine WebSocket for live updates with a periodic 'catch-up' query to handle any messages missed during disconnection
- Test WebSocket behavior when the app is put in background on mobile — connections may be killed by the OS

## Frequently asked questions

### Does Retool support WebSocket connections natively, or do I always need a JS Query?

Retool does not have a dedicated WebSocket resource type. All WebSocket connections must be opened via JS Queries using the native browser WebSocket API. There is no GUI configuration for WebSockets — it requires custom JavaScript code.

### Can I send messages from Retool to a WebSocket server (not just receive)?

Yes — store the socket instance in a window-level variable (window._retoolSocket = socket) inside your connectWebSocket JS Query. Then in a separate JS Query (triggered by a button), call window._retoolSocket.send(JSON.stringify(payload)) to send messages. The window object persists for the lifetime of the browser session.

### What is the difference between real-time updates and auto-refresh polling in Retool?

Polling (query intervals) actively re-runs a query on a timer — it pulls data from the server on a schedule. WebSocket real-time is push-based — the server sends data to the client only when something changes. Polling is simpler but generates constant database load. WebSockets are more efficient for high-frequency updates but require a WebSocket-capable server.

---

Source: https://www.rapidevelopers.com/retool-tutorial/how-to-implement-real-time-updates-in-retool
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/how-to-implement-real-time-updates-in-retool
