# How to Refresh Data Automatically in Retool

- Tool: Retool
- Difficulty: Beginner
- Time required: 15-20 min
- Compatibility: Retool Cloud and Self-hosted
- Last updated: March 2026

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

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

## Before you start

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

```
// Query settings configuration (Inspector/Settings tab):
// Query schedule: Enabled
// Interval: 30000  (30 seconds in milliseconds)

// Common intervals:
// 5000   → 5 seconds (high-frequency, use carefully — DB load)
// 10000  → 10 seconds
// 30000  → 30 seconds (good default for dashboards)
// 60000  → 1 minute (low-frequency, good for non-critical data)
// 300000 → 5 minutes (very low frequency)

// The query continues running at the interval while the browser tab is open
// 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.

```
-- SQL Query: searchCustomers
-- Trigger: Run when inputs change (enabled)
SELECT id, name, email, status
FROM customers
WHERE 
  ({{ !searchInput.value }}
   OR name ILIKE {{ '%' + searchInput.value + '%' }}
   OR email ILIKE {{ '%' + searchInput.value + '%' }})
  AND ({{ !statusFilter.value }}
       OR status = {{ statusFilter.value }})
ORDER BY name ASC
LIMIT 100;

-- This query re-runs automatically whenever:
-- searchInput.value changes (user types)
-- statusFilter.value changes (user selects)
-- 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.

```
// Option 1: Success event handler on the mutation query
// updateRecord → Success handler: Trigger query → getRecords

// Option 2: JS Query chaining (more control)
// JS Query: updateAndRefresh
try {
  await updateRecord.trigger({
    additionalScope: {
      id: table1.selectedRow.data.id,
      status: statusSelect.value
    }
  });
  
  // Refresh the table AFTER update completes
  await getRecords.trigger();
  
  utils.showNotification({ title: 'Record updated', notificationType: 'success' });
  await modal1.close();
  
} catch (err) {
  utils.showNotification({
    title: 'Update failed',
    description: err.message,
    notificationType: 'error'
  });
}
```

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

```
// Button 'Refresh' — Inspector settings:
// Label: {{ getRecords.isFetching ? 'Refreshing...' : 'Refresh' }}
// Icon: refresh-cw (or similar refresh icon)
// Disabled: {{ getRecords.isFetching }}

// onClick event handler → JS Query: manualRefresh
// JS Query: manualRefresh
await isRefreshing.setValue(true);

try {
  await getRecords.trigger();
  utils.showNotification({
    title: 'Data refreshed',
    notificationType: 'success',
    duration: 2000
  });
} catch (err) {
  utils.showNotification({
    title: 'Refresh failed',
    description: err.message,
    notificationType: 'error'
  });
} finally {
  await isRefreshing.setValue(false);
}
```

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

```
// JS Query: refreshAll — triggered by Refresh All button
await isRefreshing.setValue(true);

try {
  // Refresh all related queries in parallel
  await Promise.all([
    getOrders.trigger(),
    getOrderStats.trigger(),
    getPendingAlerts.trigger()
  ]);
  
  utils.showNotification({
    title: 'Dashboard refreshed',
    notificationType: 'success',
    duration: 2000
  });
  
} catch (err) {
  utils.showNotification({
    title: 'Refresh failed',
    description: 'Some data could not be refreshed.',
    notificationType: 'warning'
  });
} finally {
  await isRefreshing.setValue(false);
}

// In Button Inspector:
// 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.

```
// POLLING (this tutorial — suitable for most use cases)
// Pro: works with any database/API, simple to configure
// Con: adds DB load, up to N seconds stale
// Use for: dashboards, status pages, non-critical monitoring

// REAL-TIME (WebSocket — see separate tutorial)
// Pro: instant updates, efficient (no repeated queries)
// Con: requires WebSocket support on the API/DB
// Use for: live collaboration, real-time alerts, financial data

// Decision guide:
// 'Does delay of 30 seconds matter?'
//   No → polling (30s interval)
//   Yes → 'Does the backend support WebSockets?'
//     Yes → WebSockets / Supabase Realtime
//     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 code example

File: `JS Query: smartRefreshController`

```javascript
// Smart refresh controller:
// - Manual refresh button with loading state
// - Shows time since last refresh
// - Handles errors gracefully

const REFRESH_QUERIES = [getOrders, getOrderStats, getAlerts];
const QUERY_NAMES = ['getOrders', 'getOrderStats', 'getAlerts'];

const startTime = Date.now();
await isRefreshing.setValue(true);
await lastRefreshStatus.setValue('refreshing');

try {
  // Run all refresh queries in parallel
  const results = await Promise.allSettled(
    REFRESH_QUERIES.map(q => q.trigger())
  );
  
  // Check for partial failures
  const failed = results
    .map((r, i) => ({ ...r, name: QUERY_NAMES[i] }))
    .filter(r => r.status === 'rejected');
  
  const duration = ((Date.now() - startTime) / 1000).toFixed(1);
  const now = new Date().toLocaleTimeString();
  
  if (failed.length === 0) {
    // All succeeded
    await lastRefreshTime.setValue(now);
    await lastRefreshStatus.setValue('success');
    console.log(`Refresh completed in ${duration}s at ${now}`);
    
  } else {
    // Partial failure
    const failedNames = failed.map(f => f.name).join(', ');
    await lastRefreshStatus.setValue('partial');
    console.warn(`Partial refresh — failed: ${failedNames}`);
    utils.showNotification({
      title: 'Partial refresh',
      description: `Could not refresh: ${failedNames}`,
      notificationType: 'warning'
    });
  }
  
} catch (err) {
  await lastRefreshStatus.setValue('error');
  console.error('Refresh failed:', err);
  utils.showNotification({
    title: 'Refresh failed',
    description: err.message,
    notificationType: 'error'
  });
  
} finally {
  await isRefreshing.setValue(false);
}

// Refresh button label binding:
// {{ isRefreshing.value ? 'Refreshing...' : `Last updated: ${lastRefreshTime.value || 'never'}` }}
```

## Common mistakes

- **Triggering a SELECT refresh without await after a mutation, causing the refresh to run before the mutation finishes** — undefined Fix: Use await mutationQuery.trigger() before await getRecords.trigger(). The await ensures the mutation completes before the refresh query runs.
- **Setting a 5-second polling interval on a heavy JOIN query with no pagination** — undefined Fix: 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.
- **Relying on 'Run when inputs change' for a mutation query (INSERT/UPDATE)** — undefined Fix: 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.
- **Not disabling the refresh button while refreshing, allowing users to click multiple times and flood the database with requests** — undefined Fix: 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.

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

---

Source: https://www.rapidevelopers.com/retool-tutorial/how-to-refresh-data-automatically-in-retool
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/how-to-refresh-data-automatically-in-retool
