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.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Advanced |
| Time required | 20-25 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 2026 |
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.
Prerequisites
- 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
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.
1// Query: getDailySummary2// Advanced tab settings:3// Cache the results: ON4// Cache TTL (seconds): 30056-- The underlying SQL (expensive aggregation)7SELECT8 DATE_TRUNC('day', created_at) AS day,9 COUNT(*) AS orders,10 SUM(amount) AS revenue11FROM orders12WHERE created_at >= NOW() - INTERVAL '90 days'13GROUP BY 114ORDER BY 1;Expected result: Query runs slowly the first time, then returns in milliseconds for subsequent runs within the TTL.
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.
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.
1// Text component value for cache status indicator2{{ getDailySummary.servedFromCache3 ? '⚠️ Showing cached data'4 : '✓ Live data' }}56// CSS class for visual distinction7{{ getDailySummary.servedFromCache8 ? 'text-amber-600'9 : 'text-green-600' }}Expected result: A cache status indicator shows users whether they're seeing live or cached data.
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.
1// JS Query: saveAndRefresh2// Triggered by 'Save Changes' button34// 1. Run the update5await updateRecord.trigger();67// 2. Invalidate the stale cache8await getDailySummary.invalidateCache();910// 3. Re-run the read query to get fresh data11await getDailySummary.trigger();1213utils.showNotification({14 title: 'Saved',15 description: 'Record updated. Dashboard refreshed.',16 notificationType: 'success',17});Expected result: After saving, the dashboard shows fresh data instead of the previously cached result.
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 working example
1// JS Query: cachePolicyManager2// A helper that manages cache invalidation across multiple cached queries3// Run this after any write operation that affects dashboard data45// List of cached queries that need invalidation after a write6const cachedReadQueries = [7 getDailySummary,8 getTopProducts,9 getCustomerStats,10];1112// Invalidate all cached queries in parallel13await Promise.all(14 cachedReadQueries.map(q => q.invalidateCache())15);1617// Re-run all queries to populate fresh cache entries18await Promise.all(19 cachedReadQueries.map(q => q.trigger())20);2122utils.showNotification({23 title: 'Dashboard refreshed',24 description: 'All cached data has been updated.',25 notificationType: 'success',26});Common mistakes
Why it's a problem: Enabling caching on a query that uses {{ retoolContext.currentUser.email }} as a filter
How to avoid: 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.
Why it's a problem: Forgetting to invalidate the cache after a write operation, leaving users with stale data
How to avoid: Add await queryName.invalidateCache() to your write operation's success handler, followed by await queryName.trigger() to populate a fresh cache entry immediately.
Why it's a problem: Setting a very short TTL (5-10 seconds) on a slow query hoping to get both caching and freshness
How to avoid: 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
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Retool dashboard with a 'getDailySummary' query that takes 3 seconds and runs frequently. I want to cache it with a 5-minute TTL. Explain: (1) how to configure caching in the query Advanced tab, (2) how the cache key works with my {{ dateRangePicker1.value }} parameter, (3) how to show users whether they're seeing cached data using getDailySummary.servedFromCache, (4) how to invalidate the cache after a user saves an update using invalidateCache(), (5) which of my other queries should NOT be cached because they use {{ retoolContext.currentUser.email }}.
Configure Retool query caching for 'getDailySummary': Cache the results ON, TTL 300 seconds. Add a Text component showing {{ getDailySummary.servedFromCache ? 'Cached' : 'Live' }}. Write a JS Query 'invalidateAndRefresh' that calls await getDailySummary.invalidateCache() then await getDailySummary.trigger(). Explain why queries with {{ retoolContext.currentUser.email }} in the WHERE clause must never be cached.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation