# How to Use Caching Mechanisms in Retool

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

## TL;DR

Retool's query caching stores results server-side and serves them for subsequent runs with the same parameters within the TTL window. Enable it in the query's Advanced tab under 'Cache the results' and set a TTL in seconds. The cache key is based on the query's resource, SQL/config, and parameter values. Use query.invalidateCache() to bust the cache when you need fresh data.

## How Retool Query Caching Works Under the Hood

Retool's query caching is a server-side cache stored on Retool's infrastructure (for cloud) or your server (for self-hosted). When caching is enabled on a query, Retool stores the query result with a cache key derived from the resource name, query SQL/configuration, and the values of all {{ }} parameters at the time of execution.

Subsequent query runs — by any user — with matching cache keys return the stored result immediately without hitting your database. This makes caching particularly powerful for expensive aggregate queries that run frequently and don't change often (e.g., daily sales totals, static lookup tables).

The important implication: Retool's cache is shared across users. If user A runs a query and user B runs the same query within the TTL, user B gets user A's cached result. This is a feature for shared data, but a bug for user-specific queries.

## Before you start

- A Retool app with at least one slow or frequently-run query
- Understanding of SQL query parameters using {{ }} syntax
- Familiarity with the query Advanced settings tab

## Step-by-step guide

### 1. Enable caching on a query

Open a query (e.g., a slow aggregation query named 'getDailySummary'). Click the 'Advanced' tab in the query editor. Find the 'Cache the results' toggle and enable it. A 'Cache TTL (seconds)' field appears — set the duration you want results to be cached. A TTL of 300 (5 minutes) means the query result is served from cache for up to 5 minutes after the first run. For slow aggregate queries (taking 2-5 seconds), caching dramatically reduces load — subsequent runs return in milliseconds.

```
// Query: getDailySummary
// Advanced tab settings:
// Cache the results: ON
// Cache TTL (seconds): 300

-- The underlying SQL (expensive aggregation)
SELECT
  DATE_TRUNC('day', created_at) AS day,
  COUNT(*) AS orders,
  SUM(amount) AS revenue
FROM orders
WHERE created_at >= NOW() - INTERVAL '90 days'
GROUP BY 1
ORDER BY 1;
```

**Expected result:** Query runs slowly the first time, then returns in milliseconds for subsequent runs within the TTL.

### 2. Understand how cache keys work

Retool builds the cache key from three components: (1) the resource name (e.g., 'production-postgres'), (2) the query SQL/configuration template, and (3) the resolved values of all {{ }} parameters. If any parameter changes, the cache key changes and Retool makes a fresh database request. This means a query filtered by {{ select1.value }} will have separate cache entries for each distinct value of select1.value. Changing the date filter creates a new cache entry — the old TTL-valid entry is not reused.

**Expected result:** Understanding that different parameter values create different cache entries.

### 3. Check query.servedFromCache to show staleness indicators

After a query runs, check {{ queryName.servedFromCache }} — it's a boolean that is true when the result came from cache, false when fresh from the database. Use this to show a staleness indicator to users. Add a Text component near your chart or table: '{{ getDailySummary.servedFromCache ? "Cached data" : "Live data" }}'. You can also display the cache age by tracking when the query last ran using a Temporary State variable that stores Date.now() on each non-cached run.

```
// Text component value for cache status indicator
{{ getDailySummary.servedFromCache
  ? '⚠️ Showing cached data'
  : '✓ Live data' }}

// CSS class for visual distinction
{{ getDailySummary.servedFromCache
  ? 'text-amber-600'
  : 'text-green-600' }}
```

**Expected result:** A cache status indicator shows users whether they're seeing live or cached data.

### 4. Invalidate the cache when data must be fresh

After a write operation (INSERT, UPDATE, DELETE), the cached data is stale. Call await queryName.invalidateCache() to clear the cache entry and force the next run to fetch fresh data. Chain this in your save button's event handler: first run the update query, then invalidate the read query's cache, then trigger the read query again. This ensures users see updated data immediately after a write.

```
// JS Query: saveAndRefresh
// Triggered by 'Save Changes' button

// 1. Run the update
await updateRecord.trigger();

// 2. Invalidate the stale cache
await getDailySummary.invalidateCache();

// 3. Re-run the read query to get fresh data
await getDailySummary.trigger();

utils.showNotification({
  title: 'Saved',
  description: 'Record updated. Dashboard refreshed.',
  notificationType: 'success',
});
```

**Expected result:** After saving, the dashboard shows fresh data instead of the previously cached result.

### 5. Never cache user-specific queries

Because the cache is shared across all users, never enable caching on queries that filter by the current user's data using {{ retoolContext.currentUser.email }} or similar. If user A runs the query and it caches, user B will see user A's data when they run the same query within the TTL. Only cache queries returning shared data visible to all users (global aggregates, reference data, system-wide summaries).

**Expected result:** Understanding of which queries are safe to cache and which must always run fresh.

## Complete code example

File: `JS Query: cachePolicyManager`

```javascript
// JS Query: cachePolicyManager
// A helper that manages cache invalidation across multiple cached queries
// Run this after any write operation that affects dashboard data

// List of cached queries that need invalidation after a write
const cachedReadQueries = [
  getDailySummary,
  getTopProducts,
  getCustomerStats,
];

// Invalidate all cached queries in parallel
await Promise.all(
  cachedReadQueries.map(q => q.invalidateCache())
);

// Re-run all queries to populate fresh cache entries
await Promise.all(
  cachedReadQueries.map(q => q.trigger())
);

utils.showNotification({
  title: 'Dashboard refreshed',
  description: 'All cached data has been updated.',
  notificationType: 'success',
});
```

## Common mistakes

- **Enabling caching on a query that uses {{ retoolContext.currentUser.email }} as a filter** — undefined Fix: The Retool cache is shared across users. A user-filtered query cached for user A will return user A's data to user B if they run the same query within the TTL. Only cache queries returning data that is the same for all users.
- **Forgetting to invalidate the cache after a write operation, leaving users with stale data** — undefined Fix: Add await queryName.invalidateCache() to your write operation's success handler, followed by await queryName.trigger() to populate a fresh cache entry immediately.
- **Setting a very short TTL (5-10 seconds) on a slow query hoping to get both caching and freshness** — undefined Fix: A 5-second TTL provides minimal benefit for queries that take 2-3 seconds to run — the cache window is too narrow. If you need near-real-time data, disable caching and use a periodic refresh instead.

## Best practices

- Cache long-running aggregate queries (>500ms) with TTLs matching their data change frequency
- Never cache queries with user-specific WHERE clauses — the shared cache will leak one user's data to another
- Always invalidate related caches after write operations using invalidateCache() chained in the write query's success handler
- Display servedFromCache to users on dashboards so they know data may be stale
- Use longer TTLs (1 hour+) for reference/lookup data that rarely changes (country lists, category tables, config values)
- Monitor cache hit rates in the Debug Panel Performance tab to validate caching is providing benefit

## Frequently asked questions

### Where is the Retool query cache stored?

For Retool Cloud, the cache is stored on Retool's infrastructure (not in your database). For self-hosted Retool, the cache is stored in your Retool PostgreSQL database's cache table. The cache is per-organization, not per-user.

### Can I cache REST API queries in Retool, or is it only for SQL queries?

Caching is available for all Retool query types including REST API, GraphQL, SQL, and custom JavaScript queries. The cache key for REST API queries is based on the endpoint URL, HTTP method, headers, and body parameters.

### Does Retool caching work in Preview mode vs Published apps?

Cache settings apply in both preview and published apps. However, the cache is separate between preview and production — a query cached in preview does not serve cached results in the published app. This allows safe testing without affecting production cache state.

---

Source: https://www.rapidevelopers.com/retool-tutorial/how-to-use-caching-mechanisms-in-retool
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/how-to-use-caching-mechanisms-in-retool
