Skip to content

Flow Run Limits ​

A flow can cap how often and how widely it runs. Limits are admission control: they are evaluated when a run is created, and a run that would exceed a limit is refused before it exists — nothing is queued, nothing is half-started.

Every limit is opt-in and defaults to 0 ("no limit"); a flow that declares no limits is never throttled.

The limits ​

LimitMeaning
max_concurrent_runsHow many runs of this flow may be in flight at once. A run counts while it is PENDING, RUNNING, PAUSED or WAITING_APPROVAL — a run parked on an approval still holds its slot, because it is not finished.
max_runs_per_window + window_secondsHow many runs may start within a trailing window (default one hour). Self-healing: the window slides, so capacity returns without anyone editing the flow.
cooldown_secondsMinimum gap between the start of one run and the next.

Author them under Settings → General → Advanced Options → Run Limits, or directly in the flow definition:

jsonc
"run_limits": {
  "max_concurrent_runs": 2,      // at most two in flight
  "max_runs_per_window": 20,     // ...and at most 20 per window
  "window_seconds": 3600,        // rolling hour (default)
  "cooldown_seconds": 30         // ...never closer than 30s apart
}

Limits are stored in definition_json alongside step_timeout_seconds, so they version and export with the flow, and the version being executed supplies the limits that apply.

Scope ​

All triggers obey them. The interactive API, schedules, webhooks and the POST /runs path all create runs through the same service, which is where the check lives — a limit cannot be sidestepped by choosing a different entry point.

Counted per organization. A flow published by the shared org is executed by many tenants; counting globally would let one tenant starve the others, so each consumer org gets its own budget.

Child runs are exempt. A flow.run step creates a nested run, possibly of the same flow. Counting those would let a fan-out flow deadlock against itself: the parent occupies the only slot while waiting for a child that can never be admitted. Nested runs therefore skip the check entirely, and they are not counted against externally triggered runs either.

What a caller sees ​

A refused run returns 429 Too Many Requests with a message naming the limit and the current state, e.g.:

text
Run limit reached: This flow allows 2 concurrent run(s) and 2 are already in progress

429 rather than 400 is deliberate: the request was valid and retrying later is the right response.

  • API / UI — the error surfaces where any other run-creation failure does.
  • Webhooks — the delivery record stores status 429 and the message, so the skip is visible in the delivery history rather than lost.
  • Schedules — the occurrence is skipped and the schedule advances to its next slot, so a blocked flow does not make the scheduler retry every tick. last_run_at is left untouched (nothing ran). A one-time schedule is not dropped: it is retried a few minutes later, since these limits are transient. The skip is logged as schedule_run_skipped with reason: run_limit.

Concurrency correctness ​

Admission takes a row lock on the flow before counting, so two simultaneous requests cannot both observe "one slot left" and both take it. The lock is held for the remainder of the creating transaction and only serializes run creation for the same flow.

Released as open source under the AGPL-3.0-or-later license. Development is sponsored by Rexonix s.r.o.. Contact — [email protected].