Understanding exactly how and where searches operate in Bubble is important for understanding your application’s performance. For example, if my data is already on the page, do expressions that use that data refetch it and cost WU? What about :filtered?
This is based on my own research and understanding (it doesn’t come from Bubble team so don’t treat it as gospel). There’s always lots of asterisks with how Bubble works, a ton of edge cases, and lots of ‘well yes, but also…’.
Searches in Bubble
How Bubble schedules searches
Let’s suppose our page needs to show Do a search for Users in a Repeating Group. What actually happens here?
Once our page is rendered and we can see some elements (not before - page load must occur before data is fetched in most cases), the Bubble engine knows what expressions it needs to evaluate in order to do things like show or hide more elements. Some of them which relate to client-side data (e.g page width, other element visibility) will happen virtually instantly.
Others require data from the database (think, show this when Do a search for X is not empty).
When the engine knows it needs database data, it adds it to what you might call a queue. Basically, a queue of requests like ‘fetch me all Users where XYZ’.
After 10 milliseconds, or the queue has 30 pending searches, or the queue has 200 pending items across all searches (whichever comes first), the searches are batched together and run. Where possible, these are all combined into one request. You can see this in your network tab as msearch (likely meaning multiple searches).
This will return up to 20 items per search in the response.
How search results are batched + paginated
But, what about if we need 500 items, because, for example, we are displaying all rows of the repeating group immediately without lazy loading?
In this case, Bubble takes the (up to) 20 items that were returned in the first batch, and calculates the average size of each item. Bubble targets a response size of 1MB. So, if 20 items was 200KB, the next time, to fetch the rest of the rows, it’ll fetch 100 items as it expects that to be about 1MB. The limit here is 400 - if your Things are very small, it won’t just fetch them all at once - it will fetch a max of 400 in a batch.
The learning point here is that small Things (less fields/data on each field) will generally return to front-end faster, because more can be returned in a single batch, and each batch runs in sequence.
Fetching things by ID
Not all database interactions are searches. Sometimes, you’ll be fetching things by ID. You’ll never do this explicitly (though I wish Get thing by ID existed!). However, any expression like Current cell’s Invoice’s Company is fetching a Thing (Company) by its ID. This is because the Invoice stores the Company ID on its Company field. So, we have the ID, we just need Bubble to retrieve it from the database.
This is a bit more straight forward - from my understanding, Bubble just batches into fetching 200 Things at a time.
Dealing with data already on the page
This is kind of best illustrated with a diagram. Bubble is actually pretty smart (!). It tends to not fetch more data than it needs, and makes use of data it has already fetched on the page where possible.
Let’s take the case where we have a Repeating Group of Users, and we’re using :filtered on it (people sometimes do this and hope it doesn’t cost WU because it’s ‘filtering data that’s already on the client’.)
You might wanna just skip to the diagram, but it roughly goes like this:
First, Bubble checks whether all your filter constraints can be evaluated in memory. Basic comparisons like equals, not equal, contains, greater than, less than, is empty, etc. can all run client side. The big ones that can never run client side are:
- geographic filters (within X miles of Y)
- text contains, contains keywords and the negations of these
- Any field
- Range contains/range contains point
- Email field equals and email field contains
If any constraint can’t run client-side, that constraint must be sent to the server as part of the database query.
Assuming all constraints can run in memory:
- Are there under 100 items in total and we have them on the page? If yes, no need to fetch more, we can filter locally.
- Else, Bubble checks its cache of previously-fetched search results that we have downloaded already. It considers whether finishing loading would require more than 3 batches of 400 items (1200 total). If we only need to load fewer than 200 more items, or we’ve already loaded more than half and need fewer than 1200 more, it will fetch the rest and filter client side.
- If completing the load would require fetching significantly more data than you actually need (more than 1.5× what you’re asking for), Bubble avoids the wasteful fetch and filters on server instead.
Now, for geographic address list fields, date range list fields, and number range list fields, these will always operate client side (by client side, I technically mean not in the database - if it’s in a workflow, then generally it’ll be on the Bubble server. But the part you need to care about is whether the data is returned from DB as that’s what dictates your WU cost and largely the performance too).
Hope you find it interesting ![]()
Hope you find it interesting ![]()
