A quick note before this starts: we use an internal library that wraps Temporal, fully managed by the backend team at the company I work at. So this isn't a Temporal API tutorial, there are no workflow/activity code snippets here. This is the story of the problem that made us need it in the first place, and why a plain queue stopped being enough.
The old shape of the app
The app was built for a simple assumption: one organization, one tenant. Anywhere from 100 to 250 people, all seeing data scoped to their own org. Most operations, fetches, mutations, the usual, ran in the hundreds of milliseconds. Nothing about the system expected to move more data than that.
Long-running work existed, but it meant something narrower than it does now. It ran through SQS: a job gets queued, a worker picks it up, a visibility timeout governs retries if the worker dies mid-task. There was no live progress back to the UI. If something took a while, the frontend just waited, or polled a status column that said pending, running, or done. Good enough, for the scale we were at.
The request that didn't fit
Then a large organization(soon to be onboarded and would bring in good money to the business ;) asked for something the app wasn't built for: not one org, but a parent entity sitting over 100s of smaller sub-organizations underneath it, with thousands of employees across all of them needing access, scoped correctly to their own sub-org.
Structurally, this wasn't a redesign. The data model mostly held. What changed was scale, and scale changed everything downstream of it.
Why minutes, not milliseconds
An operation that used to touch one org's data now had to fan out across every sub-org underneath a parent. A permissions update that previously touched a couple hundred rows could now touch tens of thousands. Queries got heavier. Mutations, especially, started taking real time: not seconds, minutes.
And a mutation that takes minutes is a different kind of problem than a mutation that takes milliseconds. Users expect fast operations to just finish. They don't expect to sit and watch a slow one, they expect to know it's working, see it progress, and trust it'll finish even if something hiccups along the way.
Why a queue alone couldn't carry this anymore
A queue and a worker can run a slow job. What they can't do on their own is tell you much about it while it's running.
With plain SQS, a long mutation was mostly a black box: queued, then eventually done or failed. For a background job nobody's watching, that's fine. For an operation someone is actively waiting on, now taking minutes instead of milliseconds, it wasn't. We needed the UI to show live progress, not just a spinner. We needed a failure in the middle of fanning out across a hundred sub-orgs to not mean starting over, or worse, leaving the update half-applied across some sub-orgs and not others. And we needed the operation to survive a crash or a deploy mid-flight without silently going nowhere.
None of that is impossible to build on raw SQS. People do. But it means hand-rolling state tracking, hand-rolling retry logic per step, hand-rolling the bookkeeping for "which sub-orgs are done and which aren't," and hoping you got all the edge cases. That's a lot of infrastructure to maintain for every team that needs it.
Why Temporal fit
This is where Temporal made sense, not as a queue replacement, but as the layer above it that tracks what a queue can't: the actual state of a long-running operation, durably, with retries and progress built in rather than hand-built.
A migration across a hundred sub-orgs becomes a workflow with per-sub-org steps. Each step can fail and retry independently. The workflow's progress, how many sub-orgs are done, which failed, which are mid-flight, is something Temporal already tracks, not something we invented a table to approximate. And because that state is durable, a crash or a deploy doesn't lose the operation's place. It picks back up.
That's also why it sits across three layers at once, not just the backend. The backend runs the workflow. The data layer gets mutated sub-org by sub-org. And the UI gets to show something true: a live count, not a guess.
What actually changed under the hood
The data model mostly stayed the same. We didn't redesign tables or rethink how org data was structured. What changed was smaller and more specific: a few new columns to track workflow status per sub-org, a few new enum values to represent states that didn't exist before, queued, in_progress, failed, completed, per operation instead of just per job.
That's a deliberately unglamorous detail, but it matters. The architecture shift wasn't a rewrite. It was adding a layer that could track and orchestrate what was already there, not replacing what was there. For a product engineer owning a module on top of this, that's also the part that mattered most day to day: the shape of the data I worked with barely moved. What moved was how operations on that data got run and reported back.
How it fits together
The backend starts the workflow, the workflow fans out into independent, retryable steps per sub-org, and progress flows back to the UI as each one completes. One sub-org step is shown mid-retry, amber, it failed once and is trying again, entirely independent of the other sub-orgs running alongside it. That independence is the part that would've been hardest to hand-roll on a plain queue: one slow or flaky sub-org doesn't block or restart the rest.
The outcome
The honest nuance here: Temporal didn't make the fan-out operation faster. A mutation across a hundred sub-orgs still takes however long it takes, there's no shortcut around the actual volume of data being touched. What changed was the wait itself. Instead of a black box that either finished or silently failed, the UI could show real progress, a failure in one sub-org didn't take down the rest or leave the operation in an ambiguous half-done state, and a crash or deploy mid-run no longer meant starting over.
For users, that's the difference between "this feels broken" and "this is working, and I can see it." The operation was always going to take minutes. What Temporal gave us was the ability to make that time feel safe instead of uncertain.

Top comments (3)
That honesty about speed stood out: Temporal did not make the fan-out faster, but per-sub-org steps made it visible instead of a black box.
Yes, I had to mention it. It really clears it up what exactly Temporal solved in our case.
that line does the work. clears up what Temporal actually solved for you.