Backend Workflow Race condtition

Spent some time fighting a race condition in Bubble.io. Here’s what was happening, why it’s sneaky, and how I fixed it, for anyone else who runs into this.

The setup:
A backend workflow scheduled on a list (Schedule API Workflow on a list). Each instance does two things:

  1. Creates a new Thing (let’s call it an Assessment Item) — works perfectly every time.
  2. Adds that newly created item to a list field on a parent Thing (call it Assessment) using “Add.”

The symptom:
Step 1 never failed. Every item got created correctly. But step 2 was wildly inconsistent — sometimes the list had only one entry, sometimes two, never the full set I expected. No errors, no warnings. Just silent data loss.

What’s actually happening:
Scheduled workflows on a list don’t run sequentially — they run in parallel, near-simultaneously. Each workflow instance independently:

  • Reads the current state of the parent Assessment thing
  • Adds its new item to that list, in memory
  • Saves the whole list back

The problem is that “read” happens before the other instances have saved their writes. So instance A reads the list with 0 items, adds its item, and saves a list of 1. Instance B does the exact same thing, also starting from 0, and saves its own list of 1. Whichever instance saves last “wins” and overwrites the others. It’s a classic read-modify-write race condition — and Bubble’s “Add” doesn’t atomically append on the database level, it works on the data that was already loaded into that specific workflow run.

This is why it looked random: it depended purely on timing, not logic.

The fix:
Force the writes to happen sequentially instead of in parallel. A few ways to do this in Bubble:

  • Use “Schedule API Workflow on a list” with a slight incremental delay per item (even staggering by a second or two per index) so each instance’s read happens after the previous instance’s write completes - (This may not be enough as I have noticed)
  • Or switch to a recursive workflow pattern — process one item, then trigger the next item’s workflow only after the first one finishes, instead of firing them all at once
  • Or, if scale allows, do step 2 in a single workflow run after all step 1 items are created, looping through and building the full list once, instead of having each parallel instance modify the same parent record

I went with the recursive pattern since the dataset wasn’t huge, and it instantly became 100% consistent.

If you’ve never hit this, it’s worth knowing: any time multiple Bubble workflows touch the same parent record’s list field in parallel, you’re exposed to this. It’s not a Bubble bug, it’s a fundamental concurrency issue, Bubble just doesn’t make it obvious until your data starts mysteriously disappearing.

Hope this saves someone from loosing their time. :sweat_smile:

Would love to hear your thoughts if someone has faced something similar.

AI at its best

Add operator should not work that way, set list does though

I also have several loops that I built manually in backend workflows. They automatically reschedule themselves at the end of the process to handle the next item in the queue.

The problem is that this needs to be implemented very carefully. Otherwise, you can easily end up with an infinite loop and, by the end of the day, when you finally realize what happened, your credit card bill will be crying.

Bubble should have implemented this a long time ago. But until they do, at least we have the option to limit the maximum number of times a workflow can reschedule itself. That significantly reduces the risk of ending up with an infinite loop.

This is a tale as old as Bubble itself. If you are not expecting heavy requests concurrently hitting the workflow it can work. Just don’t expect it to be consistent otherwise.

Yup, have encountered this now number of times. I went with the Recursive Workflow approach as the data was not that large otherwise would have gone for separate backend workflow that add the items to the list

Recursive is the only reliable solution. Gets messy quickly when you have downstream workflows again that perform write ops on either the parent or child table row. Bubble’s code implementation is just insane and produces headaches like crazy. If you can still and are not to deeply nested in bubble. Try something else, like real code. With AI now its so much easier to get into and you control the system much more than with bubble.
These issues have been in bubble since I started 3.5 years ago, they were and still are promising native loops but they will never come with the current focus on AI. spend your time on something more useful than no-coding with bubble and learn code directly. Resources are so much more accessible today than it was.

I don’t get your statement about AI, how is the mentionned issue related to AI?

Do you suppose OP used AI, or bubble team used AI ?

Anyway, regarding the initial post. That’s always risky having X threads interacting with 1 database item.

The recursive solution is good, however a single error can ruin the entire run.
An other idea to explore would be to create the Things in a “schedule on a list” workflow, and then somehow add all those created products at once where you need them.

SAWOL isnt really setup for that as you cannot await the return values from the action and then perform further actions on them. I built a plugin that can help with that:

You would need to expose the creation of Thing that is in the List of Things as a backend Workflow that returns the created thing so you can access it via API

If you follow instruction in the plugin, you can read out & select the bubble IDs from the things generated via eg. Thing’s uniqueID is in Step 1’s key1_text .

let me know if you need assistance

The way AI wrote it.

AI writes like this, humans don’t, it’s easy to spot

If you are comfortable with code you don’t need to switch platforms. Just extend functionality.

Write your own plugins or connect your Bubble apps to your own coded services by API or websockets. There’s no need to jump ship.

Code got cheap but SWE complexity stayed the same.

I have a similar use case where I need to create a contract with 100 installments using a backend workflow.

The issue is that Bubble takes several seconds to create all 100 records, which I think is too slow for this kind of operation.

To avoid users opening the contract before everything is ready, I show a 5-second loading animation while the installments are being created.

However, there are times when those 5 seconds aren’t enough. Occasionally, only 98 installments have been created by then, leaving 2 missing from the contract.

To handle this, I created a second backend workflow that runs 15 seconds after the first one. It checks how many installments the contract has, and if the total is less than the expected 100, it runs the creation process again for the missing ones.

This workaround does the job, but in my opinion it highlights that Bubble is still too slow when it comes to creating large batches of database records. I actually came up with this solution with the help of Buildprint.

@georgecollier

Have you tried just using the API to bulk create? You can just send a single call and create all 100 entries at once, it’s faster and cheaper than what you are doing with backend workflows.

Use API to bulk create because I think the response is the unique ID of all created records, so you can use the response count of IDs to hide your loader screen…relying on arbitrary number of seconds never really works properly, since many variables related to speed of processing, fetching and returning and displaying data makes it so some random number of seconds may work sometimes but not all times.

What is consistent is checking a count to determine completeness as when you know you always need 100 records, and hide a loader only when 100 records are present it doesn’t matter about variation in how long those 100 records take.

I need to better understand how this works first. I’ll do some more research to figure out the best way to implement it.

That said, I don’t really understand why, if this capability already exists, it isn’t available as a native feature. It would make things much easier and eliminate the need for workarounds.

It is native bubble. API connector is native to bubble. Every bubble app has ability to use its own API to its own database. It really just comes down to having explored bubble enough and testing things out to find the ways to leverage native bubble features…that’s something AI can not do. I just wouldn’t rely too heavily on AI getting to the best approaches for most moderately complex implementations.

What AI does is best guesses what is correct answer, and that usually comes from averaging down all it has trained on or can scrape the web for. What that ultimately means is, AI is an average Bubble developer at best.

I’ve myself been using Bubble for the last three years, and I’m still a full-stack developer and an applied AI engineer. So yeah, there are things that aren’t great in Bubble, but it’s still good for building scalable apps. The thing is that there’s usually a workaround, but I do think Bubble should support async/await, like we have in Python or JavaScript.

Also, I raised this issue after such a long time because I hadn’t been active on the Bubble Forum or posting here. But I’ve realized it’s really useful to post here, which is why I’ve started being active and posting.

Yup that’s the 3rd way of doing it that I have written originally

Yes, “Add” shouldn’t but unfortunately it does which is really frustrating as you are simply adding “Result of step 1” to a list in Step 2

Truly useful insight. AI can definitely help speed things up, but experience still makes a big difference.

I would separate the “create child records” step from the “mutate the parent list” step.

The risky bit is every parallel workflow reading and writing the same parent field at roughly the same time. Even if each child record is created correctly, the parent list update can still lose writes.

Pattern I prefer:

  1. Create each child with a reference back to the parent.
  2. Do not update the parent’s list field inside the parallel loop.
  3. Add a queue/run id so every item in one batch can be traced.
  4. After the child creation phase is finished, run one consolidation workflow that searches children by parent + run id and sets the parent state once.
  5. Store expected_count, processed_count, failed_count, and completed_at somewhere visible.

That gives you a way to prove the batch completed, retry only the missing items, and avoid several workers fighting over the same list field.

I’m testing this as a small fixed-scope Bubble backend workflow hardening pass: queue/run ids, replay logs, and failure alerts for a few critical workflows. This race-condition case is exactly the kind of thing it is meant to catch before it becomes silent data loss.