Skip to main content
Flow Control allows you to limit how many workflow steps are executed by delaying and queuing their delivery. This feature helps to:
  • Manage resource consumption
  • Prevent violations of external API rate limits
  • Ensure workflows run within defined system constraints

How Flow Control Works

When defined limits are exceeded, Flow Control automatically queues and delays step executions instead of rejecting them. This guarantees that all steps are eventually processed while staying within configured thresholds. To configure Flow Control, you define a flow control key, a unique identifier used to group related steps under the same rate and parallelism limits. The steps that has the same flow control key respect the same constraints. There are two main parameters to configure:
  • Rate and Period: Maximum number of steps that may start within a time window
  • Parallelism: Maximum number of steps allowed to run concurrently
These parameters can be combined for fine‑grained control. For example, you can allow up to 10 steps per minute but restrict concurrency to 5 steps in parallel, ensuring more predictable load patterns.

Example

Suppose you have the following workflow:
export const { POST } = serve<{ topic: string }>(async (context) => {
  const payload = context.requestPayload

  await context.run("step-1", () => { ... });

  await context.run("step-2", () => { ... });

  await context.run("step-3", () => { ... });
})
Now imagine you trigger N workflow runs for this workflow with the following configuration:
const { workflowRunId } = await client.trigger({
  url: "https://<YOUR_WORKFLOW_ENDPOINT>/<YOUR-WORKFLOW-ROUTE>",
  flowControl: {
    key: "fw_example",
    parallelism: 7,
    rate: 3,
    period: "1m",
  }
  applyConfiguration: true,
})
Without Flow Control, all workflow runs immediately execute their steps as soon as possible. If the workflow calls an external API in a step, this would likely result in ~N concurrent requests being fired in a very short timeframe, potentially overloading services or breaching API limits.

Workflow simplified by step types

With the configuration above:
  • Rate: At most 3 steps per minute can start across all workflow runs.
  • Parallelism: At most 7 steps can be running at the same time.
Steps that exceed these limits are automatically queued and executed later.

Steps are enqueued for execution

Note that each step above corresponds to a separate workflow run. Because this workflow is sequential, each workflow run has only one pending step at a time. In workflows with parallel branches, multiple steps from the same workflow run may appear in the schedule simultaneously. Parallelism slots are consumed by running steps. If no slots are available, new steps enter the waitlist until resources free up:

Parallelism waitlist for the flow-control

Upstash Workflow does not support per-step level configuration. Meaning that you can attach a flow-control configuration to the workflow run and all the steps will inherit to the same limits. Following the analogy above, you cannot enforce parallelism limit on “green” steps natively.The context.call and context.invoke steps are exception this to this rule and accept their own flow control configuration:
  • context.call – lets you run external HTTP requests under a separate key, so you can throttle third‑party API calls independently of your workflow logic.
  • context.invoke – starts a new workflow run with its own flow control configuration. This allows the invoked workflow to run under different limits than the parent workflow, giving you more precise control.
If you want to throttle a specific context.run step, the recommended approach is to extract it into a separate workflow and call it using context.invoke() with its own flow control configuration with a stricter limits.See Advanced Per-Step Configuration for more details.

Configuration

You can configure flow control when starting a workflow run:
Configure Retry Attempt Count
import { Client } from "@upstash/workflow";

const client = new Client({ token: "<QSTASH_TOKEN>" })

const { workflowRunId } = await client.trigger({
  url: "https://<YOUR_WORKFLOW_ENDPOINT>/<YOUR-WORKFLOW-ROUTE>",
  flowControl: {
    key: "user-signup",
    parallelism: 1,
    rate: 10,
    period: 100,
  }
  applyConfiguration: true,
})
All steps within a workflow run will adhere to the specified flow control configuration.
Keep in mind that rate/period and parallelism info are kept on each step separately. If you change the rate/period or parallelism on a new deployment, the old fired ones will not be affected. They will keep their flow control configuration.During the period that old steps have not been delivered but there are also steps with new rates, Upstash Workflow will effectively allow the highest rate/period or highest parallelism. Eventually (after the old publishes are delivered), the new rate/period and parallelism will be used.