Skip to main content

Performance troubleshooting

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:

  1. From the status bar at the bottom of the app builder, open the Debug console. Switch to the Timeline tab.
  2. 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.
  3. Finally, trigger the function run from your app preview canvas.
  4. Compare the time elapsed for each function run.

Monitor sequential function runs

Monitor function runs to identify how the app talks to Retool's backend at runtime:

  1. From the status bar at the bottom of the app builder, open the Debug console. Switch to the Timeline tab.
  2. Reload the tab. Watch the waterfall as the app loads and as you interact with it.
Pattern in the waterfallWhat it meansFix
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 loadRedundant 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 timeOversized 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 itWhat 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 LibraryThe query running through your Retool resource connection, outside any app.
Function playgroundThe full function (resource query plus transformation logic) running server-side, without the app frontend.
The published appEnd-to-end time, including frontend triggers, network transfer, and rendering.
note

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/OFFSET or 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.
  • 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.
This table will have ~100k rows. Use server-side pagination: fetch 25 rows per page ordered by created_at descending, with the page number as a function parameter. Only select the six columns the table displays.
Move the notes and raw_payload fields out of the list function. Fetch them in a separate function that runs when a row's detail panel opens.

Push work to the data source

Filtering, sorting, and aggregating should happen in the database, not in function code or the browser.

note

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 WHERE and ORDER BY clauses. 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.
The status filter currently fetches all orders and filters them in code. Pass the selected status as a typed parameter to the function and filter in the SQL WHERE clause instead.

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.
The customer summary and the customer table both call getCustomers separately. Fetch once on load and share the result with both components.
getOrders shows 40 runs in its history this session, but I only changed the filter twice. Find and fix whatever is re-triggering it.

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.
Only run getAuditLog when the History tab is opened, not on app load.
Debounce the search input so searchCustomers runs 400ms after the user stops typing.

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.all too. 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.
In the Timeline tab, getCustomers, getTickets, and getTeam run sequentially on load but don't depend on each other. Run them in parallel.
Combine the lookup of the customer ID and the fetch of their orders into one function so the app makes a single call.

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.
Load the department list once when the app loads and reuse it for all dropdowns. Don't re-fetch it during the session.

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.

LayerTypical fixesWho owns it
AppPagination, triggers, parallelization, payload shaping, caching in stateThe builder, by prompting the agent or editing code
ResourceConnection configuration, environments, pointing read-heavy apps at a read replicaRetool admins, in resource settings
Data sourceIndexes, views and materialized views for heavy aggregations, warehouse sizing, stored procedures for deep joinsYour 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":

The getOrders function takes 4 seconds in the playground and returns 2MB

State data volume up front

Stating the expected volume changes the architecture the agent chooses:

This table will grow to ~500k rows

Specify trigger behavior explicitly

Say when each function should run:

Run only when the Submit button is clicked, not on load.

State security constraints

Confirm the enforcement happens in the function's query, not just in what the UI displays:

This function should only return records belonging to the signed-in user.

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.