Workload and database architecture
Fix the WU-heavy patterns first
Do not make every Bubble expression weird to save a theoretical fraction of a WU.
Start with repeating group searches, realtime admin pages, recursive workflows, backend workflows that run on lists, and workflows that fetch a whole table just to filter it later. Those are the places where small mistakes get multiplied.
Bubble workload is usually driven by:
How many Things are returned.
How large those Things are.
How often the search or workflow runs.
For example, a Product search that reruns on every filter input change is worth fixing before you worry about tiny expression differences. Use a Search button, or run Display list into the repeating group from a custom event, rather than updating the list after every keystroke.
If the Product data type has long descriptions, extracted document text, large lists, or other fields the card does not show, look at record size too. A repeating group still downloads fields that are on the returned Thing. Moving unusually heavy fields to a satellite data type can help when the list view is searched often and the heavy fields are rarely needed.
Do the obvious fixes first: fewer searches, fewer returned Things, smaller Things, and less work triggered by typing, scrolling, or live database updates.
Put limits on recursive workflows
Use Schedule API workflow on a list when it fits. It is usually easier than maintaining your own recursion.
If you do use a recursive workflow, add a count parameter. Increase it each time the workflow reschedules itself, and put a condition on the schedule action like count < 10000 (or whatever the real limit should be).
Also reduce the list each time. Process List of Orders:first item, then reschedule with List of Orders:items from #2. Do not keep passing the full list around and using an iteration number to pick the next item.
For a very hot workflow, passing a list of unique IDs can be cheaper than passing a list of Things, because the workflow can load only the current Thing by unique ID. I would not do this by default. It is less readable, so it needs to earn its keep.
Recursive workflows are fine when they have a clear stop condition. Recursive workflows with no count, no list reduction, and no sensible run limit are asking for a WU spike.
Use :group by when you need grouped data
If you need grouped or unique values, check whether :group by can answer the question before using :each item.
Suppose Aircraft has a field called Aircraft Type, and you want the Aircraft Types used by a Company.
This works:
Do a search for Aircraft:each item's Aircraft Type:unique elements
But it can require Bubble to return the matching Aircraft records and then derive the Aircraft Types.
This may be cheaper:
Do a search for Aircraft:group by Aircraft Type:each grouping's Aircraft Type
That lets Bubble answer the grouped question more directly. If you need the actual Aircraft Things afterwards, use the search that gives you the Things. If you only need the grouped Aircraft Type values, :group by is often worth testing.
The same thinking applies to other native operations. If Bubble has a native list operation like first item or items from #2, use that before adding a plugin action or server-side workaround.
Do not use workflows as variables
Do not use Make changes to a list of Things just to hold a list for later steps.
That pattern often means the workflow returns a large list, stores it as a step result, then filters it again. If you need a filtered list, write the constrained search where it is used, or pass the right list into a custom event/backend workflow as a parameter.
The bad version looks like this:
Search for all Invoices.
Use a workflow step as a temporary list.
Filter that temporary list by Customer, Status, or Date.
The better version is usually:
Do a search for Invoices where Customer = X and Status = Paid
If the workflow really does need the whole table, fine. Most workflows do not.
Do not daisy-chain filters by default
Apply search constraints once where you can.
For example, avoid building a search flow where step 1 gets all Things, step 2 filters by status, step 3 filters by date, step 4 filters by text input, and the repeating group then filters again.
Sometimes Bubble will optimise a chained expression better than expected. There was a forum example where the WU was not much worse than a normal search. It was still a pain to maintain.
Use a single constrained data source where possible. If the filter UI is too complicated for a normal Bubble expression, revisit the data model or build a deliberate search workflow. Do not keep adding hidden groups, custom states, and client-side filters until nobody can tell which filter is responsible for the result.
Filtering a small option set on the page is fine. Pulling thousands of database Things into the browser and filtering them there is not.
Keep repeating group searches small
When a repeating group is expensive, check the search before changing the UI.
Look for:
No constraints, or weak constraints.
Filters that run after the search.
Searches inside repeating group cells.
Large text fields or long lists on the returned data type.
Filter inputs that update the data source immediately.
The first fix is often to stop searching on every input change. Use a Search button, or trigger a custom event that displays the list after the user has finished changing filters.
If the Thing is too large, consider a satellite data type for the fields needed by the list view. For example, a File data type used in a document app should probably not return extracted document text every time the file browser loads. Store the searchable/list-view fields on the File, and move the extracted text to a related type that only loads on the detail/chat screen.
Only do this when the numbers make sense. A satellite data type has to be kept in sync. If the heavy fields are updated constantly, the sync workflows or database triggers can cost more than the saved search WU.
For sorting, use Bubble's dynamic "Change which field" option in Sort by where it fits. An option set for sort modes like Date created (asc), Date created (desc), Rating (asc), and Rating (desc) is usually cleaner than duplicating the whole search for each sort.
When to optimise harder
Workload cost is a product of frequency x complexity. That gives you two sides of the equation you can optimise. You can run the intensive thing less, or make it less intensive.
Do not optimise harder because an expression looks slightly untidy. A readable expression that costs 0.3 WU once a day is not the problem. A realtime repeating group search returning heavy Things all day probably is.