# Why Is My Retool Application Running Slowly?

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

## TL;DR

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.

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

## Before you start

- 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

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

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

```
-- BEFORE: returns all rows (slow)
SELECT * FROM orders ORDER BY created_at DESC;

-- AFTER: server-side pagination (fast)
SELECT * FROM orders
ORDER BY created_at DESC
LIMIT {{ table1.pageSize || 50 }}
OFFSET {{ table1.paginationOffset || 0 }};

-- Also need a count query for total pages:
SELECT COUNT(*) as total FROM orders;

-- In Table Inspector:
-- Pagination: Enabled
-- Page size: 50
-- Server-side pagination: ON
-- 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.

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

```
// Change trigger method:
// In query settings → General → Trigger:
// 'Automatic' → runs on load and on input changes
// 'Manual' → only runs when explicitly triggered

// For search queries: use 'Run when inputs change' on
// a query that is bound to searchInput.value
// This only re-runs when the search input changes,
// not on every page load.

// JS Query: initializePage (runs on load)
// Instead of many automatic queries, run critical ones in parallel:
await Promise.all([
  getCriticalData.trigger(),
  getCurrentUserInfo.trigger()
]);
// Non-critical queries run later on demand
```

**Expected result:** Page load time drops because only essential queries run automatically. Other data loads on demand.

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

```
-- Diagnose slow query:
EXPLAIN ANALYZE
SELECT * FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'pending'
AND o.created_at > NOW() - INTERVAL '30 days'
ORDER BY o.created_at DESC;

-- If EXPLAIN shows Sequential Scan on orders, add index:
-- (Run this in your database, not in Retool)
-- CREATE INDEX CONCURRENTLY idx_orders_status_created
--   ON orders(status, created_at DESC);

-- Add index for the JOIN:
-- CREATE INDEX CONCURRENTLY idx_orders_customer_id
--   ON orders(customer_id);
```

**Expected result:** After adding indexes, the same query runs in milliseconds instead of seconds.

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

```
// PROBLEM: Transformer runs on every state change
// transformer1 does heavy computation
// It re-runs whenever searchInput.value changes
// This causes visible lag on every keystroke

// SOLUTION: Move heavy work to a JS Query with manual trigger
// JS Query: computeExpensiveReport (trigger: Manual)
const data = getOrders.data || [];

// Heavy computations
const grouped = _.groupBy(data, 'customer_id');
const enriched = Object.entries(grouped).map(([id, orders]) => ({
  customerId: id,
  orderCount: orders.length,
  totalRevenue: _.sumBy(orders, 'amount'),
  avgOrderValue: _.meanBy(orders, 'amount'),
  lastOrder: _.maxBy(orders, 'created_at')?.created_at
}));

return _.orderBy(enriched, ['totalRevenue'], ['desc']);

// Trigger this query only when needed:
// - On page load (one time)
// - After a data refresh
// NOT on every UI interaction
```

**Expected result:** Heavy computation only runs when needed, eliminating the typing lag caused by transformers re-executing on every keystroke.

### 6. 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 code example

File: `SQL Query + JS Query: performanceOptimizedLoading`

```javascript
// === Optimized page initialization ===
// JS Query: initPage (trigger: Automatic, runs on page load)

console.log('Page init started:', Date.now());

try {
  // Run critical above-the-fold queries in parallel
  await Promise.all([
    getTableData.trigger(),   // First visible table
    getDashboardStats.trigger() // KPI cards
  ]);
  
  console.log('Critical queries done:', Date.now());
  
  // Non-critical queries run after UI is visible
  // These are triggered by user interaction, not on load
  // Examples:
  //   getExportData → triggered by Export button click
  //   getFullAuditLog → triggered by Audit tab click
  //   getHistoricalCharts → triggered by date range change
  
} catch (err) {
  console.error('Page init failed:', err);
  utils.showNotification({
    title: 'Loading failed',
    description: 'Some data could not be loaded. Try refreshing.',
    notificationType: 'warning'
  });
}

// === Server-side paginated SQL query ===
// Query: getTableData
// SELECT
//   id, customer_name, status, created_at, total_amount
//   -- NOT SELECT * — only fetch columns you display
// FROM orders o
// WHERE
//   ({{ searchInput.value }} = '' OR customer_name ILIKE {{ '%' + searchInput.value + '%' }})
//   AND ({{ statusFilter.value }} = '' OR status = {{ statusFilter.value }})
// ORDER BY created_at DESC
// LIMIT {{ table1.pageSize || 50 }}
// OFFSET {{ table1.paginationOffset || 0 }}
```

## Common mistakes

- **Using SELECT * in queries that fetch large tables** — undefined Fix: Explicitly list only the columns your app displays. Fetching 40 columns when you only show 5 wastes bandwidth and slows serialization.
- **Setting all queries to 'Automatic' trigger without considering what actually needs to load on page startup** — undefined Fix: 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.
- **Putting complex data transformations in a transformer that re-runs on every UI change** — undefined Fix: 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.
- **Building a 50+ component app on a single page** — undefined Fix: 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.

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

---

Source: https://www.rapidevelopers.com/retool-tutorial/why-is-retool-application-running-slowly
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/why-is-retool-application-running-slowly
