Skip to main content
RapidDev - Software Development Agency
retool-tutorial

How to Implement Real-Time Updates in Retool

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.

What you'll learn

  • Open a WebSocket connection inside a Retool JS Query and handle incoming messages
  • Update a temporary state variable from WebSocket onmessage to refresh UI in real time
  • Implement connection lifecycle management: open, close, reconnect
  • Use query polling intervals as a simpler alternative to WebSockets
  • Handle WebSocket authentication and heartbeat keepalive patterns
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced8 min read30-40 minRetool Cloud and Self-hostedLast updated March 2026RapidDev Engineering Team
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.

Quick facts about this guide
FactValue
ToolRetool
DifficultyAdvanced
Time required30-40 min
CompatibilityRetool Cloud and Self-hosted
Last updatedMarch 2026

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.

Prerequisites

  • 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.

typescript
1// JS Query: connectWebSocket
2// Set to 'Run on page load'
3
4const WS_URL = 'wss://your-realtime-endpoint.com/feed';
5
6const socket = new WebSocket(WS_URL);
7
8socket.onopen = async () => {
9 await wsStatus.setValue('connected');
10 // Send auth if needed:
11 // socket.send(JSON.stringify({ type: 'auth', token: 'Bearer xyz' }));
12};
13
14socket.onmessage = async (event) => {
15 let parsed;
16 try {
17 parsed = JSON.parse(event.data);
18 } catch {
19 parsed = { raw: event.data };
20 }
21
22 // Get current messages and prepend new one
23 // Note: do NOT read wsMessages.value immediately after setValue —
24 // setValue is async and the value won't reflect the update yet.
25 const current = wsMessages.value || [];
26 const updated = [parsed, ...current].slice(0, 100); // Keep last 100
27 await wsMessages.setValue(updated);
28};
29
30socket.onerror = async (err) => {
31 await wsStatus.setValue('error');
32 utils.showNotification({
33 title: 'WebSocket error',
34 description: 'Connection failed. Check your endpoint URL.',
35 notificationType: 'error',
36 });
37};
38
39socket.onclose = async () => {
40 await wsStatus.setValue('disconnected');
41};

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.

typescript
1// Enhanced connectWebSocket with reconnection
2let reconnectAttempts = 0;
3const MAX_RECONNECTS = 5;
4
5const connect = async () => {
6 const socket = new WebSocket('wss://your-endpoint.com/feed');
7
8 socket.onopen = async () => {
9 reconnectAttempts = 0;
10 await wsStatus.setValue('connected');
11 };
12
13 socket.onclose = async () => {
14 await wsStatus.setValue('disconnected');
15 if (reconnectAttempts < MAX_RECONNECTS) {
16 reconnectAttempts++;
17 const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
18 await wsStatus.setValue(`reconnecting (${reconnectAttempts}/${MAX_RECONNECTS})...`);
19 setTimeout(connect, delay);
20 }
21 };
22
23 socket.onmessage = async (event) => {
24 const current = wsMessages.value || [];
25 await wsMessages.setValue([JSON.parse(event.data), ...current].slice(0, 100));
26 };
27};
28
29await connect();

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

Complete working example

JS Query: connectWebSocket (full implementation)
1// JS Query: connectWebSocket
2// Full WebSocket implementation with auth, heartbeat, and reconnection
3// Set this query to 'Run on page load'
4
5const WS_URL = retoolContext.configVars.WS_ENDPOINT || 'wss://echo.websocket.org';
6const AUTH_TOKEN = retoolContext.configVars.WS_AUTH_TOKEN;
7
8let socket = null;
9let heartbeatTimer = null;
10let reconnectAttempts = 0;
11const MAX_RECONNECTS = 5;
12
13const clearHeartbeat = () => {
14 if (heartbeatTimer) clearInterval(heartbeatTimer);
15};
16
17const startHeartbeat = (ws) => {
18 heartbeatTimer = setInterval(() => {
19 if (ws.readyState === WebSocket.OPEN) {
20 ws.send(JSON.stringify({ type: 'ping' }));
21 }
22 }, 30000);
23};
24
25const connect = async () => {
26 socket = new WebSocket(WS_URL);
27
28 socket.onopen = async () => {
29 reconnectAttempts = 0;
30 await wsStatus.setValue('connected');
31 if (AUTH_TOKEN) {
32 socket.send(JSON.stringify({ type: 'auth', token: AUTH_TOKEN }));
33 }
34 startHeartbeat(socket);
35 };
36
37 socket.onmessage = async (event) => {
38 if (event.data === 'pong') return; // Ignore heartbeat responses
39 try {
40 const parsed = JSON.parse(event.data);
41 const current = wsMessages.value || [];
42 // Note: setValue() is async — the value is NOT immediately readable
43 // after this call in the same synchronous context.
44 await wsMessages.setValue([parsed, ...current].slice(0, 200));
45 } catch {
46 console.log('Non-JSON message:', event.data);
47 }
48 };
49
50 socket.onerror = async () => {
51 await wsStatus.setValue('error');
52 };
53
54 socket.onclose = async () => {
55 clearHeartbeat();
56 await wsStatus.setValue('disconnected');
57 if (reconnectAttempts < MAX_RECONNECTS) {
58 reconnectAttempts++;
59 const delay = Math.min(1000 * 2 ** reconnectAttempts, 30000);
60 setTimeout(connect, delay);
61 } else {
62 await wsStatus.setValue('failed — reload the page');
63 }
64 };
65};
66
67await connect();

Common mistakes

Why it's a problem: Calling await wsMessages.setValue(newData) then immediately reading wsMessages.value expecting the new data

How to avoid: 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.

Why it's a problem: Creating a new WebSocket connection every time the query runs without closing the previous one

How to avoid: 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.

Why it's a problem: Forgetting that JS Queries in Retool run in a sandboxed environment without persistent global scope

How to avoid: 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

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I need to implement real-time updates in Retool using WebSockets. My WebSocket endpoint is wss://api.myapp.com/live-feed and requires a Bearer token header. I need: (1) a JS Query that opens the WebSocket on page load, (2) updates a Temporary State array 'wsMessages' with each incoming message (keeping last 100), (3) tracks connection status in 'wsStatus', (4) implements exponential backoff reconnection, (5) sends a ping every 30 seconds as a heartbeat. Write the complete JS Query with the setValue() async gotcha in mind.

Retool Prompt

Create a Retool JS Query 'connectWebSocket' that: opens WebSocket at wss://endpoint.com, stores incoming JSON messages in Temporary State 'wsMessages' using await wsMessages.setValue([parsed, ...wsMessages.value].slice(0,100)), updates 'wsStatus' string state on open/close/error, and implements 5-attempt exponential backoff reconnection. Set it to run on page load.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Learning is great. Shipping is faster with help.

Our engineers have built 600+ apps on Retool and the tools around it. If your project needs to be live sooner than your learning curve allows — book a free consultation.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.