Style guide

Workload guardrails for real apps

Optimise the parts that can run away

Do not spend the first week of a build shaving tiny amounts of workload units off simple workflows.

Do add guardrails around Bubble features that can repeat, reschedule, reload, or watch lots of data. That is where WU spikes usually come from.

I would check these first:

  • Realtime searches on busy pages.

  • Recursive backend workflows.

  • Repeating groups with broad searches.

  • Searches that return a whole table before filtering.

  • Backend workflows used as temporary variable storage.

  • List processing that passes more Things than the next step needs.

The question is not whether the action uses WU. It does. The useful question is whether Bubble is loading, watching, or processing data the user does not need.

Realtime data should be intentional

Realtime updates are useful when the page needs live data. Chat messages, collaborative screens, active status, and time-sensitive dashboards are normal examples.

They are expensive when an admin page watches high-churn data that the user only needs as a snapshot.

This might be a repeating group that searches for active Jobs, Orders, Files, or Messages. If backend workflows, webhooks, imports, or other users keep changing those Things, every open browser tab may keep receiving updates.

For admin tables, reports, exports, and review screens, decide whether the user needs live updates or just a fresh list after they click a filter, refresh button, tab, or workflow action.

If stale data would cause the user to make the wrong decision, keep realtime. Just narrow the search and keep the repeating group light.

Recursive workflows need hard limits

Recursive workflows are fine when they are controlled. They are painful when they reschedule themselves forever.

For most list processing, I would now look at "Schedule API workflow on a list" first. It is usually easier than passing the remainder of a list through a recursive workflow, and it avoids a lot of old manual recursion logic.

If you do need recursion, pass less data each time and add a count parameter.

For example, a backend workflow processing Invoices can process List of Invoices:first item, then reschedule itself with List of Invoices:items from #2. Pass iteration_count, set it to iteration_count + 1, and only schedule the next run when it is below your limit.

The exact limit depends on the workflow. The important bit is that a bug, bad search, or unexpected list size cannot schedule unlimited work.

Do not fetch the table and filter afterwards

If Bubble can constrain the search in the database, do that before the result reaches the browser or workflow.

Watch for:

  • Fuzzy search plugins that download a large candidate list and filter client-side.

  • Live text input searches that run a new search for every character.

  • Repeating groups that use :filtered after a broad Do a search for.

  • Backend workflows that fetch a large list and filter it later.

If a user searches Users by bio, prefer a search constraint such as bio contains keyword(s) Input's value where that fits the product. If you need to search multiple fields often, a searchField text field can work: keep it in sync with a backend trigger, then search that field directly.

That only makes sense when the app searches far more often than it writes. If the field changes constantly, the backend trigger may cost more than it saves.

The same point applies to list fields. A list on a Thing is fine when it stays small. If it is regularly approaching 100 items, or if it is displayed and filtered often, ask whether it should be a search instead.

Do not remodel every list just because lists can be expensive. Remodel the lists that are large, growing, searched often, or passed through backend workflows.

Backend workflows should not be variable storage

Do not use backend workflow actions to "store" a list for later steps.

One bad pattern is using "Make changes to a list of Things" when the workflow is not really changing those Things. The action is being used as a variable holder, then the list gets filtered later.

That can force Bubble to return a large table, then do more work to filter it. It also makes the workflow annoying to read because the next developer has to work out whether the step changes data or just exists to hold a value.

Use normal Bubble expressions when the value is only needed inside the workflow. For imports, exports, AI jobs, or long-running backend work, use a proper status data type so the app can track progress deliberately.

Expensive list processing should be simplified first

When a list-processing workflow is expensive, check the Bubble expression first.

One common example is using a plugin action or server-side action to pop the first item from a list. Bubble can often do that natively: process :first item, then pass :items from #2 to the next step.

Native Bubble expressions are usually easier to debug than a plugin workaround. They also make it clearer what the workflow is doing when someone else has to maintain it.

Custom code or an external service can still be right for heavy maths, document parsing, or large transformations. I just would not start there if the workflow is expensive because it is passing a huge list around badly.

What to check when WU suddenly spikes

When WU jumps, do not rewrite random workflows.

First check what changed: a new repeating group, dashboard, search, backend trigger, recursive workflow, import, webhook, or scheduled job.

Then check:

  • Are backend workflows recursively scheduling themselves?

  • Does each recursive path have a count, limit, or stop condition?

  • Did a backend trigger start firing on a field that changes frequently?

  • Did a page start loading a large table or high-churn data type?

  • Are users leaving an admin page open with realtime searches running?

  • Is a search returning a whole table before :filtered, fuzzy search, or list filtering runs?

  • Is a workflow making changes to a list when it only needs an inline expression?

  • Is a list page loading a large text field, file content field, or rich text field that it does not display?

That last one is easy to miss. A File data type might start with metadata and extracted text on the same Thing because it was fast for the MVP. Later, the file browser gets expensive because every list view loads the heavy extracted text too.

Move the heavy field to a satellite data type if the app often needs the file list without the file content.

Guardrails to add before the app is busy

For production apps, I would usually want:

  • Database constraints on important searches.

  • Pagination or progressive loading for large lists.

  • Recursive workflows with count parameters and hard stop conditions.

  • Backend triggers scoped to real field transitions, not any change to a Thing.

  • Admin screens that load only the records they need.

  • Heavy fields kept off data types used for lightweight list views.

  • A deliberate decision on which pages need realtime updates.

  • Comments on non-obvious workflow limits, batch sizes, and operational assumptions.

If the app is small, some inefficient searches are probably fine for now. If the search runs for every active user, watches a busy data type, or sits inside a backend workflow, fix it earlier.

Was this helpful?