Performance troubleshooting
Troubleshooting strategies for improving app performance.
As your Retool apps become more complex and interact with an increasing number of data sources, it’s important to monitor performance and make improvements where needed.
This guide provides instructions for diagnosing slow app performance, and strategies that you can use to address the issues you identify. Most fixes can be made by prompting the agent; this guide includes example prompt patterns throughout.
Investigate
First, investigate potential bottlenecks using the following strategies.
Check function run history
Open the Data tab and select a function to open the Function playground, then check its History tab. If a function has run far more times than the user interactions that should trigger it, the app is over-fetching. This usually means trigger() is wired to a state or component whose dependencies change too often, and it's the fastest signal that the problem is trigger behavior rather than function speed.
To resolve this issue, Retool recommends eliminating redundant fetches.
Monitor the duration of each function separately
Run a test to determine whether the function run itself is slow, or whether the issue lies in the frontend:
- From the status bar at the bottom of the app builder, open the Debug console. Switch to the Timeline tab.
- Then, open the Data tab and select a function to open the Function playground. Enter sample parameters and run the function manually. This measures the function's server-side execution (the resource query plus any transformation logic) without any frontend involvement.
- Finally, trigger the function run from your app preview canvas.
- Compare the time elapsed for each function run.
- If a function is fast in the playground but slow in the app, the problem lives in the frontend (waterfalls, over-triggering, or rendering).
- If the function is equally slow in the Function playground and in the app preview, the problem is in the function or the data source. Further investigate the source of the slow function run.
Monitor sequential function runs
Monitor function runs to identify how the app talks to Retool's backend at runtime:
- From the status bar at the bottom of the app builder, open the Debug console. Switch to the Timeline tab.
- Reload the tab. Watch the waterfall as the app loads and as you interact with it.
| Pattern in the waterfall | What it means | Fix |
|---|---|---|
| Requests forming a stair-step, each starting only after the previous one finishes. | Functions are running sequentially. If a later function doesn't use the earlier one's results, they can run in parallel. | Parallelize independent functions. |
| The same function called multiple times within one interaction or page load | Redundant fetching: multiple components triggering their own copies of the same request. | Eliminate redundant fetches. |
| A single request with a very large response size or long download time | Oversized payload, or time is being spent server-side, in the function or the data source rather than the browser. | Keep payloads small, and triangulate latency |
Identify the source of the slow function run
When a specific function is slow, run the same underlying resource query in several places and compare timings. Each location isolates a different layer of the stack. Recording these four numbers before you prompt for a fix also gives the agent concrete targets, and gives you a baseline to verify improvement against.
| Where to run it | What it measures |
|---|---|
| Directly in your data source's own client (e.g., the BigQuery console, a Snowflake worksheet, the Databricks SQL editor, or psql) | Pure database execution time, with no Retool involvement. |
| Query Library | The query running through your Retool resource connection, outside any app. |
| Function playground | The full function (resource query plus transformation logic) running server-side, without the app frontend. |
| The published app | End-to-end time, including frontend triggers, network transfer, and rendering. |
Measure app timings against the published app where possible. The editor preview adds overhead, and functions that modify data may require approval in the editor before they run.
Interpret results
Once you have the function run duration in each environment, interpret the results:
- Slow everywhere: The problem is in the database. Look at missing indexes, query structure, warehouse sizing, or cluster autoscaling/cold starts. This fix belongs to your database or data platform team, not the app.
- Fast in the published app, slow in the Query Library: The gap is in the connection path: resource configuration, network distance between Retool and your data source, connection setup, or authentication overhead. Review your resource settings with your Retool admin.
- Fast in the Query Library, slow in the Function playground: The transformation logic inside the function is the bottleneck. Simplify the post-query steps or push that work into the query itself.
- Fast in the Function playground, slow in the published app: The app's wiring is the problem: sequential waterfalls, over-triggering, redundant calls, or rendering large datasets. Refer to the best practices to improve your app performance.
Apply targeted performance best practices
Use the best practices in the following sections to improve the performance of your app and its functions.
Paginate large datasets
Large tables are the most common performance pitfall. Fetch only what the user can see, and let the data source do the limiting.
- Paginate as close to the data as possible. Where you trim the dataset decides how far every row travels:
- Best, in the data source: Apply
LIMIT/OFFSETor cursor conditions in the query itself, passing page size and page number (or a cursor) as typed function parameters. Only one page ever leaves the database. - Sometimes fine, in the function: If the resource can't paginate, slicing the result in function code keeps extra rows out of the browser. The full dataset still crosses from the data source into Retool, so this strategy only applies for moderate result sets.
- Worst, in the browser: Never fetch everything and paginate client-side in React. Every row is transferred over the network and held in memory.
- Best, in the data source: Apply
- Select only the columns the table displays. For example, avoid
SELECT *, especially on wide tables or tables with large text or JSON columns. - Fetch heavy detail fields (long descriptions, JSON blobs, attachments) in a separate function that runs only when the user opens a record's detail view.
Push work to the data source
Filtering, sorting, and aggregating should happen in the database, not in function code or the browser.
Ask the LLM to use query pushdown and keep the application layer thin. Related conventions include thin views in Django and skinny controllers in Ruby on Rails. These terms signal that data-intensive operations should be delegated to the database or resource layer rather than performed in UI or request-handling code.
- Pass filter and sort values as function parameters and apply them in
WHEREandORDER BYclauses. Don't fetch everything and filter the results in TypeScript. - Use database aggregation (
COUNT,SUM,GROUP BY) for metrics and summaries rather than computing them from raw rows. - Keep unavoidable transformation logic inside the function, where it runs server-side, so the browser receives small, ready-to-render results.
- Because function parameters are strongly typed and queries are parameterized, values are passed safely rather than concatenated into SQL. Keep it that way: if you edit code directly, never build SQL strings from user input.
Eliminate redundant fetches
Each piece of data should be fetched by one function call and shared across the components that need it via React state or props, not fetched separately by each component.
- If the Debug console or a function's run history shows the same function firing multiple times per interaction, ask the agent to consolidate the triggers.
- Watch for re-fetch loops: a
trigger()call inside an effect whose dependencies change on every render fires continuously. Monitor the Timeline tab in the Debug console for function runs that continue while the app sits idle.
Control when functions run
Running too many functions on page load delays the time until users can interact with the app.
- Load only the data needed for the initial view. Defer everything else until the user asks for it.
- Don't fetch data for hidden UI. Tabs, modals, drawers, and detail panels should trigger their functions when opened, not on app load.
- Be explicit with the agent about trigger behavior for every function: on load, on click, or when a specific value changes. In code, this corresponds to where
trigger()is called. - Debounce search and filter inputs (300-500ms) so a function runs after the user stops typing, not on every keystroke.
Parallelize independent functions
When one function's input doesn't depend on another function's output, they should run concurrently.
- Use the Timeline tab in the Debug console to spot candidates: sequential stair-step requests where the later request doesn't need the earlier one's data. A common example is a page that loads customers, open tickets, and team members back-to-back when all three are independent.
- In frontend code, issue independent function calls concurrently (for example, with
Promise.all) rather than awaiting them one after another. - The same applies inside a function. If one function makes several independent resource calls, run them with
Promise.alltoo. Otherwise the database round trips happen sequentially even though the browser made a single request. - Truly dependent steps, where step two needs step one's result, can be composed into a single function so the round trips between browser and backend collapse into one. This only removes the browser-to-Retool hops; the resource calls inside the function still run one after another unless they're independent and parallelized.
Cache stable reference data
Data that rarely changes, such as dropdown options, categories, country lists, and role definitions, shouldn't be re-fetched on every interaction.
- Fetch reference data once on app load and hold it in app state for the rest of the session.
- For data that changes occasionally, refresh on a long interval (for example, every 10-15 minutes) instead of on every render or interaction.
- For read-only reference data, you can also ask the agent to enable stale-while-revalidate caching on the function's hook (
cachePolicy: 'stale-while-revalidate'), which serves cached results immediately while refreshing in the background. This applies to read-only functions only. - For expensive client-side computation over already-loaded data, memoize the result so it isn't recomputed on every render.
Keep response payloads small
Row counts aren't the only cost; the amount of data returned in each row also matters. Large responses slow network transfer and browser rendering even when the query itself is fast.
- Return only the fields the UI consumes. Shape the response inside the function.
- Avoid returning Base64-encoded files, images, or large nested JSON in list responses. Return references instead, and load heavy content on demand.
- For large read-only result sets that the UI can render incrementally, ask the agent to use a streaming resource method. Streaming improves time-to-first-byte and avoids buffering the full response server-side. It applies when the function doesn't need to transform the result.
Match fixes to the right layer
Some performance work doesn't belong in the app at all. Route each fix to the layer that owns it.
| Layer | Typical fixes | Who owns it |
|---|---|---|
| App | Pagination, triggers, parallelization, payload shaping, caching in state | The builder, by prompting the agent or editing code |
| Resource | Connection configuration, environments, pointing read-heavy apps at a read replica | Retool admins, in resource settings |
| Data source | Indexes, views and materialized views for heavy aggregations, warehouse sizing, stored procedures for deep joins | Your database administrator or data platform team |
If your latency triangulation shows the database is the bottleneck, bring the numbers to the owning team rather than trying to prompt around it. A missing index fixed at the source benefits every app and workflow that touches that table.
Prompting the agent for performance
The agent fixes performance issues best when your prompt includes evidence you've gathered.
Ground prompts in numbers
A grounded prompt gets a better result than a vague one like "the orders page is slow":
State data volume up front
Stating the expected volume changes the architecture the agent chooses:
Specify trigger behavior explicitly
Say when each function should run:
State security constraints
Confirm the enforcement happens in the function's query, not just in what the UI displays:
Scope the change
Name the specific function or component to change, and say what should stay untouched, so a performance fix doesn't ripple into unrelated parts of the app.
Verify after each fix
Re-run the playground timing or reload with the Timeline tab open, and compare against your baseline before moving to the next issue.