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.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Advanced |
| Time required | 30-40 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 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
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.
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.
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.
1// JS Query: connectWebSocket2// Set to 'Run on page load'34const WS_URL = 'wss://your-realtime-endpoint.com/feed';56const socket = new WebSocket(WS_URL);78socket.onopen = async () => {9 await wsStatus.setValue('connected');10 // Send auth if needed:11 // socket.send(JSON.stringify({ type: 'auth', token: 'Bearer xyz' }));12};1314socket.onmessage = async (event) => {15 let parsed;16 try {17 parsed = JSON.parse(event.data);18 } catch {19 parsed = { raw: event.data };20 }2122 // Get current messages and prepend new one23 // 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 10027 await wsMessages.setValue(updated);28};2930socket.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};3839socket.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.
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.
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.
1// Enhanced connectWebSocket with reconnection2let reconnectAttempts = 0;3const MAX_RECONNECTS = 5;45const connect = async () => {6 const socket = new WebSocket('wss://your-endpoint.com/feed');78 socket.onopen = async () => {9 reconnectAttempts = 0;10 await wsStatus.setValue('connected');11 };1213 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 };2223 socket.onmessage = async (event) => {24 const current = wsMessages.value || [];25 await wsMessages.setValue([JSON.parse(event.data), ...current].slice(0, 100));26 };27};2829await connect();Expected result: Connection drops are handled automatically with exponential backoff reconnection.
Complete working example
1// JS Query: connectWebSocket2// Full WebSocket implementation with auth, heartbeat, and reconnection3// Set this query to 'Run on page load'45const WS_URL = retoolContext.configVars.WS_ENDPOINT || 'wss://echo.websocket.org';6const AUTH_TOKEN = retoolContext.configVars.WS_AUTH_TOKEN;78let socket = null;9let heartbeatTimer = null;10let reconnectAttempts = 0;11const MAX_RECONNECTS = 5;1213const clearHeartbeat = () => {14 if (heartbeatTimer) clearInterval(heartbeatTimer);15};1617const startHeartbeat = (ws) => {18 heartbeatTimer = setInterval(() => {19 if (ws.readyState === WebSocket.OPEN) {20 ws.send(JSON.stringify({ type: 'ping' }));21 }22 }, 30000);23};2425const connect = async () => {26 socket = new WebSocket(WS_URL);2728 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 };3637 socket.onmessage = async (event) => {38 if (event.data === 'pong') return; // Ignore heartbeat responses39 try {40 const parsed = JSON.parse(event.data);41 const current = wsMessages.value || [];42 // Note: setValue() is async — the value is NOT immediately readable43 // 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 };4950 socket.onerror = async () => {51 await wsStatus.setValue('error');52 };5354 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};6667await 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation