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

How to Refresh Data Automatically in Retool

Auto-refresh data in Retool using the query's 'Query schedule' (periodic trigger) setting — set an interval in milliseconds and the query re-runs on that schedule. For reactive refresh when a user filter changes, enable 'Run when inputs change'. For post-mutation refresh, call await getRecords.trigger() in your JS Query. For real-time, use WebSockets instead of polling.

What you'll learn

  • How to configure a query's periodic trigger (auto-refresh interval in milliseconds)
  • How to use 'Run when inputs change' for reactive data fetching
  • How to trigger a data refresh programmatically with await query.trigger()
  • How to build a manual refresh button with loading state feedback
  • The difference between polling (auto-refresh) and real-time updates (WebSockets)
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner9 min read15-20 minRetool Cloud and Self-hostedLast updated March 2026RapidDev Engineering Team
TL;DR

Auto-refresh data in Retool using the query's 'Query schedule' (periodic trigger) setting — set an interval in milliseconds and the query re-runs on that schedule. For reactive refresh when a user filter changes, enable 'Run when inputs change'. For post-mutation refresh, call await getRecords.trigger() in your JS Query. For real-time, use WebSockets instead of polling.

Quick facts about this guide
FactValue
ToolRetool
DifficultyBeginner
Time required15-20 min
CompatibilityRetool Cloud and Self-hosted
Last updatedMarch 2026

Data Refresh Patterns in Retool

Keeping data fresh in a Retool app can happen in three ways: scheduled polling (query runs on a timer), reactive refresh (query re-runs when a filter or input changes), or programmatic refresh (JS Query explicitly triggers a data query after a mutation).

Polling is the simplest — set a query to re-run every 30 seconds for dashboards that need near-real-time data. Reactive refresh uses Retool's 'Run when inputs change' trigger so a search or filter query automatically re-runs when the user types. Programmatic refresh is explicit: after inserting a record, call await getRecords.trigger() to reload the table.

This tutorial covers all three patterns with practical examples, plus how to build a manual refresh button with a loading indicator for user-controlled refreshes.

Prerequisites

  • A Retool account with an app and at least one data query
  • Basic understanding of JS Queries and event handlers
  • Familiarity with Retool state variables (for loading indicators)
  • A database or API resource configured in Retool

Step-by-step guide

1

Set up a periodic polling trigger for a query

Select the query in the Code panel. Open the query's Advanced or Settings tab. Look for 'Query schedule' or 'Periodic refresh'. Enable it and set the interval in milliseconds (e.g., 30000 for 30 seconds, 60000 for 1 minute). The query will automatically re-run at this interval as long as the app is open. This is ideal for dashboards, status monitors, and live operation tables.

typescript
1// Query settings configuration (Inspector/Settings tab):
2// Query schedule: Enabled
3// Interval: 30000 (30 seconds in milliseconds)
4
5// Common intervals:
6// 5000 → 5 seconds (high-frequency, use carefully — DB load)
7// 10000 → 10 seconds
8// 30000 → 30 seconds (good default for dashboards)
9// 60000 → 1 minute (low-frequency, good for non-critical data)
10// 300000 → 5 minutes (very low frequency)
11
12// The query continues running at the interval while the browser tab is open
13// It stops when the user closes or navigates away from the app

Expected result: The query automatically re-runs every N milliseconds. The table or chart updates with fresh data without any user action.

2

Enable 'Run when inputs change' for reactive queries

For search and filter queries, enable 'Run when inputs change' in the query's General settings. When this is enabled, the query automatically re-runs whenever any {{ }} input binding's value changes. For example, a SQL query with {{ searchInput.value }} in its WHERE clause will re-run every time the user types in the search box. This is the reactive pattern — no periodic timer needed.

typescript
1-- SQL Query: searchCustomers
2-- Trigger: Run when inputs change (enabled)
3SELECT id, name, email, status
4FROM customers
5WHERE
6 ({{ !searchInput.value }}
7 OR name ILIKE {{ '%' + searchInput.value + '%' }}
8 OR email ILIKE {{ '%' + searchInput.value + '%' }})
9 AND ({{ !statusFilter.value }}
10 OR status = {{ statusFilter.value }})
11ORDER BY name ASC
12LIMIT 100;
13
14-- This query re-runs automatically whenever:
15-- searchInput.value changes (user types)
16-- statusFilter.value changes (user selects)
17-- No event handler needed

Expected result: Changing the search input or filter dropdown triggers the query to re-run automatically. The table updates in real time as the user types.

3

Refresh data programmatically after mutations

After a mutation query (INSERT, UPDATE, DELETE) completes, trigger the SELECT query to refresh the table with updated data. Do this in your mutation query's Success event handler, or in a JS Query that chains the mutation and refresh with await. This ensures the table always reflects the current database state after user actions.

typescript
1// Option 1: Success event handler on the mutation query
2// updateRecord → Success handler: Trigger query → getRecords
3
4// Option 2: JS Query chaining (more control)
5// JS Query: updateAndRefresh
6try {
7 await updateRecord.trigger({
8 additionalScope: {
9 id: table1.selectedRow.data.id,
10 status: statusSelect.value
11 }
12 });
13
14 // Refresh the table AFTER update completes
15 await getRecords.trigger();
16
17 utils.showNotification({ title: 'Record updated', notificationType: 'success' });
18 await modal1.close();
19
20} catch (err) {
21 utils.showNotification({
22 title: 'Update failed',
23 description: err.message,
24 notificationType: 'error'
25 });
26}

Expected result: After saving a change, the table refreshes to show the updated data without the user needing to manually reload.

4

Build a manual refresh button with loading state

Give users a way to manually refresh data with a dedicated Refresh button. The button triggers the data query and shows a loading spinner or disables itself while the refresh is running. Use a state variable for the loading indicator and the isFetching property of the query to disable the button during fetching.

typescript
1// Button 'Refresh' — Inspector settings:
2// Label: {{ getRecords.isFetching ? 'Refreshing...' : 'Refresh' }}
3// Icon: refresh-cw (or similar refresh icon)
4// Disabled: {{ getRecords.isFetching }}
5
6// onClick event handler → JS Query: manualRefresh
7// JS Query: manualRefresh
8await isRefreshing.setValue(true);
9
10try {
11 await getRecords.trigger();
12 utils.showNotification({
13 title: 'Data refreshed',
14 notificationType: 'success',
15 duration: 2000
16 });
17} catch (err) {
18 utils.showNotification({
19 title: 'Refresh failed',
20 description: err.message,
21 notificationType: 'error'
22 });
23} finally {
24 await isRefreshing.setValue(false);
25}

Expected result: Clicking Refresh triggers the query. The button shows 'Refreshing...' and is disabled while the query runs. It returns to 'Refresh' when complete.

5

Refresh multiple related queries together

Some apps have multiple related queries that should all refresh together — for example, a main data table plus a summary statistics panel. Refresh them in parallel using Promise.all() for better performance than sequential refreshes.

typescript
1// JS Query: refreshAll — triggered by Refresh All button
2await isRefreshing.setValue(true);
3
4try {
5 // Refresh all related queries in parallel
6 await Promise.all([
7 getOrders.trigger(),
8 getOrderStats.trigger(),
9 getPendingAlerts.trigger()
10 ]);
11
12 utils.showNotification({
13 title: 'Dashboard refreshed',
14 notificationType: 'success',
15 duration: 2000
16 });
17
18} catch (err) {
19 utils.showNotification({
20 title: 'Refresh failed',
21 description: 'Some data could not be refreshed.',
22 notificationType: 'warning'
23 });
24} finally {
25 await isRefreshing.setValue(false);
26}
27
28// In Button Inspector:
29// Disabled: {{ getOrders.isFetching || getOrderStats.isFetching }}

Expected result: All three queries run simultaneously. Total refresh time equals the slowest query, not the sum of all three.

6

Understand when to use polling vs real-time WebSockets

Polling (periodic trigger) re-runs a query on a timer. It is simple and works with any data source but adds database load and has inherent delay (up to the interval length). WebSocket-based real-time updates push changes from the server the instant they happen. Use polling for dashboards where a 10-30 second delay is acceptable. Use WebSockets for collaborative tools, live chat, or stock/monitoring data where subsecond updates are needed.

typescript
1// POLLING (this tutorial — suitable for most use cases)
2// Pro: works with any database/API, simple to configure
3// Con: adds DB load, up to N seconds stale
4// Use for: dashboards, status pages, non-critical monitoring
5
6// REAL-TIME (WebSocket — see separate tutorial)
7// Pro: instant updates, efficient (no repeated queries)
8// Con: requires WebSocket support on the API/DB
9// Use for: live collaboration, real-time alerts, financial data
10
11// Decision guide:
12// 'Does delay of 30 seconds matter?'
13// No → polling (30s interval)
14// Yes → 'Does the backend support WebSockets?'
15// Yes → WebSockets / Supabase Realtime
16// No → polling with shorter interval (5-10s)

Expected result: You have chosen the appropriate refresh strategy for your use case based on data freshness requirements.

Complete working example

JS Query: smartRefreshController
1// Smart refresh controller:
2// - Manual refresh button with loading state
3// - Shows time since last refresh
4// - Handles errors gracefully
5
6const REFRESH_QUERIES = [getOrders, getOrderStats, getAlerts];
7const QUERY_NAMES = ['getOrders', 'getOrderStats', 'getAlerts'];
8
9const startTime = Date.now();
10await isRefreshing.setValue(true);
11await lastRefreshStatus.setValue('refreshing');
12
13try {
14 // Run all refresh queries in parallel
15 const results = await Promise.allSettled(
16 REFRESH_QUERIES.map(q => q.trigger())
17 );
18
19 // Check for partial failures
20 const failed = results
21 .map((r, i) => ({ ...r, name: QUERY_NAMES[i] }))
22 .filter(r => r.status === 'rejected');
23
24 const duration = ((Date.now() - startTime) / 1000).toFixed(1);
25 const now = new Date().toLocaleTimeString();
26
27 if (failed.length === 0) {
28 // All succeeded
29 await lastRefreshTime.setValue(now);
30 await lastRefreshStatus.setValue('success');
31 console.log(`Refresh completed in ${duration}s at ${now}`);
32
33 } else {
34 // Partial failure
35 const failedNames = failed.map(f => f.name).join(', ');
36 await lastRefreshStatus.setValue('partial');
37 console.warn(`Partial refresh — failed: ${failedNames}`);
38 utils.showNotification({
39 title: 'Partial refresh',
40 description: `Could not refresh: ${failedNames}`,
41 notificationType: 'warning'
42 });
43 }
44
45} catch (err) {
46 await lastRefreshStatus.setValue('error');
47 console.error('Refresh failed:', err);
48 utils.showNotification({
49 title: 'Refresh failed',
50 description: err.message,
51 notificationType: 'error'
52 });
53
54} finally {
55 await isRefreshing.setValue(false);
56}
57
58// Refresh button label binding:
59// {{ isRefreshing.value ? 'Refreshing...' : `Last updated: ${lastRefreshTime.value || 'never'}` }}

Common mistakes

Why it's a problem: Triggering a SELECT refresh without await after a mutation, causing the refresh to run before the mutation finishes

How to avoid: Use await mutationQuery.trigger() before await getRecords.trigger(). The await ensures the mutation completes before the refresh query runs.

Why it's a problem: Setting a 5-second polling interval on a heavy JOIN query with no pagination

How to avoid: High-frequency polling on expensive queries hammers your database. Either optimize the query (add indexes, pagination), reduce the polling interval, or switch to a WebSocket-based push approach.

Why it's a problem: Relying on 'Run when inputs change' for a mutation query (INSERT/UPDATE)

How to avoid: Never set mutation queries to 'Run when inputs change' — they will fire on every user keystroke, creating duplicate records. Always set mutations to Manual trigger.

Why it's a problem: Not disabling the refresh button while refreshing, allowing users to click multiple times and flood the database with requests

How to avoid: Set the button's Disabled property to {{ getRecords.isFetching }} to prevent multiple simultaneous refresh triggers.

Best practices

  • Choose polling intervals based on actual data freshness requirements — 30-60 seconds is appropriate for most business dashboards.
  • Use 'Run when inputs change' for filter/search queries rather than periodic polling — it is more efficient and provides instant feedback.
  • Always await mutation queries before triggering refresh queries — without await, the refresh may run before the mutation completes.
  • Use Promise.all() to refresh multiple queries in parallel — it is significantly faster than sequential refreshes for dashboards with multiple panels.
  • Disable the refresh button using {{ queryName.isFetching }} while a refresh is in progress to prevent double-refresh race conditions.
  • For mission-critical real-time data (financial, safety, collaborative), use WebSocket-based solutions (Supabase Realtime, Pusher) instead of polling.
  • Consider the database impact of frequent polling across many concurrent users — 100 users × 10-second poll = 600 queries/minute on your database.

Still stuck?

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

ChatGPT Prompt

I have a Retool operations dashboard that shows order data. I need: (1) the main orders table to auto-refresh every 30 seconds, (2) a search filter that instantly re-queries when the user types, (3) a 'Refresh All' button that updates both the table and a stats panel simultaneously, and (4) after a user updates an order status, the table should refresh automatically. Show me how to configure each of these patterns with the correct trigger settings and JS Query code.

Retool Prompt

In my Retool app, configure the following refresh patterns: (1) set getOrders SQL query to run on a 30-second periodic schedule, (2) set searchCustomers SQL query to 'Run when inputs change' since it references {{ searchInput.value }}, (3) write a JS Query that uses await Promise.all([getOrders.trigger(), getStats.trigger()]) for a manual refresh, and (4) add a refresh button that shows 'Refreshing...' using {{ getOrders.isFetching }}.

Frequently asked questions

What is the minimum polling interval for Retool query auto-refresh?

Retool allows intervals as low as 100ms, but practical minimums depend on your use case and database capacity. For most business apps, 10-30 seconds is appropriate. Sub-second polling is possible but creates significant database load, especially with many concurrent users. For sub-second updates, use WebSocket-based real-time solutions instead.

Does Retool's auto-refresh continue running when the browser tab is in the background?

Modern browsers throttle JavaScript timers (including Retool's polling intervals) for background tabs, typically to a minimum interval of 1 second. If a tab is hidden for extended periods, some browsers may suspend timers entirely. The polling resumes when the tab becomes active again. This is a browser behavior, not a Retool limitation.

How is 'Run when inputs change' different from setting a periodic trigger?

'Run when inputs change' fires the query immediately whenever a {{ }} binding's value changes — it is event-driven and instant. A periodic trigger fires on a time schedule regardless of any changes. Use 'Run when inputs change' for reactive search/filter queries, and periodic triggers for data that should stay fresh over time without user interaction.

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.