The most impactful Retool performance fixes in order: (1) reduce query payload below 1.6MB by limiting columns and adding pagination, (2) disable page-load execution on queries that users only need sometimes, (3) enable caching on slow aggregate queries, (4) split large apps into multipage apps, (5) debounce input-triggered queries. The Debug Panel Performance tab grades your app from A-F and identifies specific issues.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Advanced |
| Time required | 30-40 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 2026 |
Why Retool Apps Get Slow and How to Fix Each Cause
A slow Retool app is usually caused by one of five issues: (1) query payloads too large — fetching thousands of rows or wide result sets, (2) too many queries on page load — every query runs simultaneously when the page opens, (3) slow database queries — missing indexes, expensive joins, or unoptimized SQL, (4) too many components — apps with 50+ components have high render overhead, (5) no caching — identical queries running repeatedly instead of serving from cache.
The Debug Panel Performance tab in Retool grades each issue from A-F and shows specific recommendations. This tutorial walks through fixing each grade below B, from easiest to most impactful.
Prerequisites
- A Retool app that loads slowly or has user-reported performance issues
- Access to the Retool Debug Panel (click the bug icon)
- Admin or Editor access to modify query settings and app configuration
Step-by-step guide
Run a performance audit in the Debug Panel
Open your app in edit mode and click the bug icon (bottom-left) to open the Debug Panel. Switch to the Performance tab. Retool grades your app in several categories: Queries (payload size, count), Components (total count), and Network (response times). Review each grade — anything below B is worth fixing. Note the specific issues listed under each grade: 'Query X returns 50,000 rows' or 'App has 65 components — consider splitting into pages'. Start with the F and D grades first.
Expected result: Performance audit shows grades for each category with specific issues and recommendations.
Reduce query payload size below 1.6MB
The most common performance killer is fetching too much data. A query returning 10,000 rows with 20 columns generates megabytes of JSON that the browser must parse and render. Fix this by: (1) SELECT only the columns you display, (2) add a WHERE clause to filter down the result set, (3) implement LIMIT for all queries without pagination. Retool's recommended payload limit is 1.6MB per query — the Debug Panel shows each query's payload size.
1-- ❌ Before: SELECT * returns all 40 columns2SELECT * FROM orders WHERE status = 'active';34-- ✅ After: SELECT only displayed columns5SELECT6 id, order_number, customer_name, status,7 total_amount, created_at8FROM orders9WHERE status = 'active'10ORDER BY created_at DESC11LIMIT 500; -- Cap at 500 rows until pagination is addedExpected result: Query payload drops below 1.6MB. Page load time decreases proportionally.
Disable page-load queries that aren't needed immediately
Every query with 'Run on page load' enabled fires simultaneously when the app opens. If your app has 10 queries all running on load, the page must wait for all 10 to complete. Audit each query: is it needed for the initial view, or only when the user interacts? For queries behind tabs, modals, or filter selections — disable 'Run on page load' and instead trigger them from an event handler or via 'Run when inputs change' (which only runs when their inputs have values).
Expected result: Page load only runs the minimum required queries. Tab/modal queries load on demand.
Enable server-side pagination for large tables
Client-side pagination (the default) fetches all rows then paginates in the browser. Server-side pagination fetches only the current page's rows. Enable server-side pagination by modifying your SQL query to use LIMIT and OFFSET tied to the table's pagination properties: {{ table1.pageSize }} (default 10) and {{ table1.pageIndex * table1.pageSize }}. Also add a count query for total pages. This keeps payloads consistently small regardless of table size.
1-- Server-side paginated query2SELECT3 id, name, status, created_at4FROM orders5WHERE status = {{ statusFilter.value || 'active' }}6ORDER BY created_at DESC7LIMIT {{ table1.pageSize }}8OFFSET {{ table1.pageIndex * table1.pageSize }};910-- Companion count query for total pages11SELECT COUNT(*) AS total12FROM orders13WHERE status = {{ statusFilter.value || 'active' }};1415-- In the Table Inspector:16-- Pagination type: Manual (server-side)17-- Total rows: {{ countQuery.data[0].total }}Expected result: Table fetches only 10-50 rows per page instead of all records. Query payload stays under 100KB regardless of total data size.
Enable caching on slow aggregate queries
Aggregate queries (COUNT, SUM, AVG, GROUP BY) are often the slowest in your app because they scan entire tables. Enable caching in the query's Advanced tab. Set a TTL appropriate for how often the underlying data changes — a daily summary query might cache for 5 minutes, a static product catalog for 1 hour. After enabling caching, use await queryName.invalidateCache() in write operation success handlers to ensure stale data doesn't persist after modifications.
Expected result: Slow aggregate queries serve cached results in milliseconds. Database load decreases significantly.
Split large apps into multipage apps
Retool's Debug Panel gives a component count performance grade. Apps with 50+ components have high render overhead. The solution is converting a single large app into a multipage app: click Settings → Convert to multipage app. Create separate pages for distinct sections (Dashboard, Orders, Customers, Settings). Each page loads only its own queries and components — reducing the initial load and improving perceived performance significantly.
Expected result: App splits into pages. Each page loads faster with fewer components and queries per view.
Debounce input-triggered queries
If a query has 'Run when inputs change' enabled and is triggered by a text input (like a search box), it fires on every keystroke. A user typing 'customer name' fires the query 13 times. Add a debounce by using a JS Query as intermediary: instead of the query directly watching the text input, watch a Temporary State variable. A 'debounce' JS Query (not on page load) sets the state variable with a 300ms delay after input changes.
1// In the text input's On change event handler:2// Trigger JS Query 'debounceSearch'34// JS Query: debounceSearch5// Triggered by search input's On change6let debounceTimer;7clearTimeout(debounceTimer);8debounceTimer = setTimeout(async () => {9 await searchQuery.trigger();10}, 300);1112// searchQuery references searchInput.value in its SQL WHERE clauseExpected result: Search query fires once after the user stops typing (300ms pause) instead of on every keystroke.
Complete working example
1-- Fully optimized paginated query with server-side filtering2-- Combines: specific column selection, server-side pagination,3-- dynamic filtering, and prepared statement parameters45SELECT6 o.id,7 o.order_number,8 c.name AS customer_name,9 o.status,10 o.total_amount,11 o.created_at12FROM orders o13JOIN customers c ON o.customer_id = c.id14WHERE15 o.status = COALESCE({{ statusSelect.value || null }}, o.status)16 AND o.created_at >= COALESCE({{ dateRangePicker1.value[0] || null }}, '2000-01-01'::date)17 AND o.created_at <= COALESCE({{ dateRangePicker1.value[1] || null }}, NOW())18 AND (19 {{ !searchInput.value }}20 OR c.name ILIKE {{ '%' + (searchInput.value || '') + '%' }}21 OR o.order_number ILIKE {{ '%' + (searchInput.value || '') + '%' }}22 )23ORDER BY o.created_at DESC24LIMIT {{ table1.pageSize || 20 }}25OFFSET {{ (table1.pageIndex || 0) * (table1.pageSize || 20) }};Common mistakes
Why it's a problem: Using SELECT * in all queries without realizing it returns dozens of unnecessary columns
How to avoid: Audit each query's result schema in the Debug Panel. Identify columns that are never displayed and remove them from the SELECT list.
Why it's a problem: Enabling 'Run on page load' on 15 queries that all fire simultaneously on page open
How to avoid: Categorize each query: required for initial view (keep page load), or only needed on interaction (disable page load, trigger from event handler). Aim for 3-5 queries on initial page load.
Why it's a problem: Having search input directly trigger a SQL query on every keystroke via 'Run when inputs change'
How to avoid: Add a debounce using a setTimeout in a JS Query between the input and the database query. This prevents database queries on every keystroke and is critical for search functionality.
Best practices
- Run a Debug Panel Performance audit before and after optimizations to measure actual improvement
- Target query payloads under 1.6MB each — this is the single biggest performance lever in most apps
- Use SELECT specific columns instead of SELECT * — it reduces payload, network transfer, and memory usage
- Implement server-side pagination for any table showing more than 200 rows
- Cache slow aggregate queries with TTLs matching data change frequency
- Keep total component count per page under 40 — consider multipage apps for larger applications
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
My Retool app is loading slowly. The Debug Panel shows: (1) query 'getAllOrders' returns 500KB and fetches 10,000 rows with SELECT *, (2) 15 queries run on page load, (3) app has 70 components. For each problem, give me the specific fix: the optimized SQL for getAllOrders with server-side pagination ({{ table1.pageSize }}, {{ table1.pageIndex }}), which queries I should disable on page load, and how to split into a multipage app.
Optimize a Retool app with: (1) SQL query using SELECT specific_columns with LIMIT {{ table1.pageSize }} and OFFSET {{ table1.pageIndex * table1.pageSize }}, (2) query caching in Advanced tab with TTL 300 for aggregate queries, (3) debounce pattern for search input using setTimeout in JS Query, (4) multipage app conversion with global variables for cross-page state. Show the Debug Panel Performance grade targets for each fix.
Frequently asked questions
What is the recommended maximum payload size for Retool queries?
Retool recommends keeping individual query payloads under 1.6MB (1,600,000 bytes). The Debug Panel Performance tab shows each query's payload size and flags any that exceed this threshold. Payloads above 5MB can cause the browser to become unresponsive during rendering.
How many components can a Retool app have before performance degrades?
Retool's Debug Panel gives a performance grade for component count. Apps with under 30 components typically get A grades. 30-50 components is B-C range. Above 50 components, Retool recommends splitting into a multipage app. Component count is the cumulative total including hidden components — hidden components still consume memory and render time.
Does the number of queries in a Retool app affect performance even if they don't run on page load?
Queries that don't run on page load (disabled from 'Run on page load') do not affect initial load time. They only consume resources when triggered. However, having many queries in your app's codebase can slow down the Retool editor itself — consider consolidating queries with similar purposes.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation