Retool apps slow down for four main reasons: queries returning too many rows (keep payloads under 1.6MB), too many queries running on page load, complex JS transformers running on every state change, and apps with too many components on a single page. Start with the Debug Panel Performance tab to identify the bottleneck before optimizing.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Beginner |
| Time required | 20-30 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 2026 |
Diagnosing and Fixing Retool Performance Problems
A Retool app that loads slowly or lags on user interaction is almost always caused by one of four things: queries that return too many rows at once, too many queries running simultaneously on page load, heavy JS transformer work that runs on every state change, or too many components in a single page view.
Before optimizing, always measure first. Retool's Debug Panel has a Performance tab (or Waterfall tab depending on your version) that shows the execution time and payload size for every query. This tells you exactly where the time is going — a 3-second page load might be entirely due to one query returning a 5MB payload.
This tutorial walks through diagnosing each performance problem and applying the right fix: pagination, query optimization, page splitting, or transformer refactoring.
Prerequisites
- A Retool app that is loading slowly (or you want to prevent it from slowing down)
- Editor access to the app
- Basic understanding of SQL (for pagination queries)
- Familiarity with the Debug Panel (bug icon in the bottom toolbar)
Step-by-step guide
Open the Debug Panel Performance tab and measure query times
Click the bug icon in the bottom toolbar to open the Debug Panel. Switch to the Waterfall or Performance tab. Reload the app to capture all page-load queries. The waterfall shows every query execution with its duration (in milliseconds), payload size (in KB), and success/failure status. Sort by duration (longest first) and by payload size to identify the two most common causes of slowness.
Expected result: The waterfall shows all queries with timing and payload data. You can identify the slowest and largest queries.
Add server-side pagination to high-row-count queries
If any query returns more than a few hundred rows, add LIMIT and OFFSET to the SQL to fetch only one page at a time. In the Table component Inspector, enable 'Server-side pagination' and bind the offset to {{ table1.paginationOffset }} and the limit to your page size. Retool's table automatically manages pagination state — you only need to bind the SQL parameters.
1-- BEFORE: returns all rows (slow)2SELECT * FROM orders ORDER BY created_at DESC;34-- AFTER: server-side pagination (fast)5SELECT * FROM orders6ORDER BY created_at DESC7LIMIT {{ table1.pageSize || 50 }}8OFFSET {{ table1.paginationOffset || 0 }};910-- Also need a count query for total pages:11SELECT COUNT(*) as total FROM orders;1213-- In Table Inspector:14-- Pagination: Enabled15-- Page size: 5016-- Server-side pagination: ON17-- Total count query: {{ countOrders.data[0].total }}Expected result: The table loads in under 200ms by fetching 50 rows at a time instead of thousands. Users navigate pages with the built-in pagination controls.
Disable unnecessary page-load query auto-running
In the Code panel, click each query. Under its General settings, look at 'Trigger method'. Queries set to 'Automatic' run when the page loads and whenever their inputs change. If a query only needs to run after user interaction (e.g., a search query, a form submission result), change its trigger to 'Manual' and run it explicitly from event handlers. Reducing page-load queries from 10 to 3 can cut page-load time dramatically.
1// Change trigger method:2// In query settings → General → Trigger:3// 'Automatic' → runs on load and on input changes4// 'Manual' → only runs when explicitly triggered56// For search queries: use 'Run when inputs change' on7// a query that is bound to searchInput.value8// This only re-runs when the search input changes,9// not on every page load.1011// JS Query: initializePage (runs on load)12// Instead of many automatic queries, run critical ones in parallel:13await Promise.all([14 getCriticalData.trigger(),15 getCurrentUserInfo.trigger()16]);17// Non-critical queries run later on demandExpected result: Page load time drops because only essential queries run automatically. Other data loads on demand.
Optimize slow SQL queries and add database indexes
In the Debug Panel waterfall, if a query takes over 500ms, the bottleneck is likely in the database. Open the query in the Code panel and click 'Run' with EXPLAIN or EXPLAIN ANALYZE prepended to the SQL to see the query plan. Look for sequential scans on large tables — these indicate missing indexes. Add indexes on columns used in WHERE, JOIN ON, and ORDER BY clauses.
1-- Diagnose slow query:2EXPLAIN ANALYZE3SELECT * FROM orders o4JOIN customers c ON c.id = o.customer_id5WHERE o.status = 'pending'6AND o.created_at > NOW() - INTERVAL '30 days'7ORDER BY o.created_at DESC;89-- If EXPLAIN shows Sequential Scan on orders, add index:10-- (Run this in your database, not in Retool)11-- CREATE INDEX CONCURRENTLY idx_orders_status_created12-- ON orders(status, created_at DESC);1314-- Add index for the JOIN:15-- CREATE INDEX CONCURRENTLY idx_orders_customer_id16-- ON orders(customer_id);Expected result: After adding indexes, the same query runs in milliseconds instead of seconds.
Refactor heavy transformers into on-demand JS Queries
Transformers run automatically whenever any of their inputs change. A complex transformer with _.groupBy, nested maps, and date formatting running on every keystroke in a search box will visibly lag the UI. Move expensive computation out of transformers and into JS Queries that run manually or on a specific trigger. Use simple transformers only for lightweight, pure data reshaping.
1// PROBLEM: Transformer runs on every state change2// transformer1 does heavy computation3// It re-runs whenever searchInput.value changes4// This causes visible lag on every keystroke56// SOLUTION: Move heavy work to a JS Query with manual trigger7// JS Query: computeExpensiveReport (trigger: Manual)8const data = getOrders.data || [];910// Heavy computations11const grouped = _.groupBy(data, 'customer_id');12const enriched = Object.entries(grouped).map(([id, orders]) => ({13 customerId: id,14 orderCount: orders.length,15 totalRevenue: _.sumBy(orders, 'amount'),16 avgOrderValue: _.meanBy(orders, 'amount'),17 lastOrder: _.maxBy(orders, 'created_at')?.created_at18}));1920return _.orderBy(enriched, ['totalRevenue'], ['desc']);2122// Trigger this query only when needed:23// - On page load (one time)24// - After a data refresh25// NOT on every UI interactionExpected result: Heavy computation only runs when needed, eliminating the typing lag caused by transformers re-executing on every keystroke.
Split large apps into multiple pages or modules
If an app has more than 30-50 components on a single page, Retool needs to render and reconcile all of them on every state change. Consider splitting the app into a multipage app where each page has focused functionality. Use Retool Modules to share components between pages without duplicating them. This also improves page load time because each page only loads its own queries.
Expected result: Each page loads faster with fewer components and queries. Navigation between pages is quick because pages load on demand.
Complete working example
1// === Optimized page initialization ===2// JS Query: initPage (trigger: Automatic, runs on page load)34console.log('Page init started:', Date.now());56try {7 // Run critical above-the-fold queries in parallel8 await Promise.all([9 getTableData.trigger(), // First visible table10 getDashboardStats.trigger() // KPI cards11 ]);12 13 console.log('Critical queries done:', Date.now());14 15 // Non-critical queries run after UI is visible16 // These are triggered by user interaction, not on load17 // Examples:18 // getExportData → triggered by Export button click19 // getFullAuditLog → triggered by Audit tab click20 // getHistoricalCharts → triggered by date range change21 22} catch (err) {23 console.error('Page init failed:', err);24 utils.showNotification({25 title: 'Loading failed',26 description: 'Some data could not be loaded. Try refreshing.',27 notificationType: 'warning'28 });29}3031// === Server-side paginated SQL query ===32// Query: getTableData33// SELECT34// id, customer_name, status, created_at, total_amount35// -- NOT SELECT * — only fetch columns you display36// FROM orders o37// WHERE38// ({{ searchInput.value }} = '' OR customer_name ILIKE {{ '%' + searchInput.value + '%' }})39// AND ({{ statusFilter.value }} = '' OR status = {{ statusFilter.value }})40// ORDER BY created_at DESC41// LIMIT {{ table1.pageSize || 50 }}42// OFFSET {{ table1.paginationOffset || 0 }}Common mistakes
Why it's a problem: Using SELECT * in queries that fetch large tables
How to avoid: Explicitly list only the columns your app displays. Fetching 40 columns when you only show 5 wastes bandwidth and slows serialization.
Why it's a problem: Setting all queries to 'Automatic' trigger without considering what actually needs to load on page startup
How to avoid: Audit each query's trigger setting. Change secondary/detail queries to 'Manual' trigger and run them from explicit event handlers (button click, row select). Only run critical above-the-fold data automatically.
Why it's a problem: Putting complex data transformations in a transformer that re-runs on every UI change
How to avoid: Move heavy computation to a JS Query. Trigger it once on page load and on data refresh, not on every keystroke. Simple display formatting (e.g., format a date) is fine in a transformer; multi-step aggregation is not.
Why it's a problem: Building a 50+ component app on a single page
How to avoid: Split the app into multiple pages. Retool renders and reconciles all page components on every state change — fewer components per page means faster UI updates.
Best practices
- Always add LIMIT and OFFSET to queries binding to Table components — never return unbounded result sets.
- Run a maximum of 3-5 queries automatically on page load; trigger the rest on user demand.
- Keep query payloads under 1.6MB per query — select only the columns you need, not SELECT *.
- Add database indexes on all columns used in WHERE, JOIN ON, and ORDER BY clauses in frequently run queries.
- Use Promise.all() for independent page-load queries to run them in parallel instead of sequentially.
- Avoid putting heavy computation (groupBy, nested maps) in transformers — they re-run on every input change. Use JS Queries instead.
- Use the Debug Panel waterfall as your first step when investigating slowness — measure before optimizing.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
My Retool app takes 8+ seconds to load and lags when I type in search boxes. It has one main page with 20 queries and a Table showing thousands of rows. Diagnose the likely causes and show me: (1) how to use the Debug Panel to identify the bottleneck, (2) how to add server-side pagination with LIMIT/OFFSET to the main query, (3) how to change secondary queries from Automatic to Manual trigger, and (4) how to use Promise.all() for parallel loading of critical queries.
In my Retool app, the SQL query 'getOrders' is taking 4 seconds and returning 50,000 rows. Help me: (1) add server-side pagination with LIMIT {{ table1.pageSize }} OFFSET {{ table1.paginationOffset }}, (2) write a count query for total pages, (3) configure the Table component to use server-side pagination, and (4) show me how to use EXPLAIN ANALYZE to check if an index would help.
Frequently asked questions
What is Retool's recommended maximum payload size per query?
Retool recommends keeping query response payloads under 1.6MB. Payloads above this threshold cause noticeable slowdowns because Retool must serialize and deserialize the data on each query execution and state change. If a query regularly exceeds this, add server-side pagination to fetch data in smaller batches.
How many queries should run automatically on a Retool page load?
Aim for 3-5 queries running automatically on page load — enough to populate the main visible components. Every additional automatic query adds to the time before the app is interactive. Secondary data (audit logs, export data, charts below the fold) should use Manual triggers and load on user demand.
Does Retool have a built-in performance profiler?
Yes. The Debug Panel (bug icon, bottom toolbar) has a Waterfall or Performance tab that shows every query's execution time and payload size in a timeline view. This is your first diagnostic tool. For database-level profiling, use EXPLAIN ANALYZE in your SQL queries. For JavaScript profiling, use the browser DevTools Performance tab.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation