DEV Community

Cover image for Your Wait Step Works Once. Then It Stops Waiting
The Unmeshed Team
The Unmeshed Team

Posted on

Your Wait Step Works Once. Then It Stops Waiting

We set a Wait step to pause three seconds inside a loop. First time through, perfect. Second time through, it just stops waiting. Like it forgot its one job.

Here's the twist: nothing's broken. The engine's doing exactly what we told it to do. We just didn't realize what we were actually telling it.

Why it happens

A Wait step takes a function that returns a timestamp, waitUntil, and most people write it as an offset from when the step started:

(steps, context) => {
  const startDate = new Date(steps.__self.start);
  return {
    waitUntil: startDate.getTime() + 3_000, // wait 3 seconds
  };
};
Enter fullscreen mode Exit fullscreen mode

That start value is set once, the first time the step runs, and it never updates. On the first pass through the loop, that's fine, the timestamp is three seconds in the future and the engine waits as expected.

On the second pass, the While loop re-runs the same Wait step instance. It still reads the original start value, which is now well in the past. As far as the engine's concerned, waitUntil has already been satisfied, so it continues immediately. It's not skipping the wait. It's correctly honoring a timestamp that's already expired.

This is the part that trips people up: a Wait step inside a loop doesn't behave like sleep() in regular code. The engine re-evaluates the same node on every pass, it doesn't restart it with fresh state.

The fix: track a timestamp per iteration

Instead of computing waitUntil once from a fixed start time, store a separate target timestamp for every loop iteration, and only calculate a new one the first time that iteration runs:

(steps, context) => {
  const iterations = steps.__self.output.result?.iterations || [];
  const currentIter = steps.loop.output.iteration;

  if (iterations.length <= currentIter) {
    iterations.push(Date.now() + 3_000); // wait 3 seconds from *now*
  }

  return {
    waitUntil: iterations[currentIter],
    iterations,
    loop: currentIter,
  };
};
Enter fullscreen mode Exit fullscreen mode

This holds up under retries and restarts, since the timestamp for a given iteration is stored and reused rather than recalculated, and each loop cycle gets its own delay measured from when that cycle actually began, not from whenever the Wait step was first created.

Other ways to handle it

  • Store the timestamp in process context if more than one step needs to reference the same schedule.
  • Push the timing logic into the While condition itself, works well for simple polling loops where ""stop once 3 seconds have passed"" can be expressed directly.
  • Move the Wait outside the loop entirely, if what's actually needed is one delay before the loop starts, not a delay on every pass.

The short version

Treat a Wait step like any other piece of state: make it idempotent, compute targets relative to now instead of a fixed start time when it's inside a loop, and log the iteration index so timing bugs are easy to spot later. None of this is a limitation. It's just not sleep(), and it behaves differently the moment you understand what it's actually doing on each pass."

Top comments (0)