Runtime limits
These are the limits the cloud runtime enforces on a deployed agent today. Local runs (sapiom_dev_agents_run_local) behave differently; see Test locally.
Summary
Section titled “Summary”| Limit | Value | What happens when it is reached |
|---|---|---|
| Total run duration | No limit | — |
| One step attempt | 15 minutes, or the step’s timeoutMs | The attempt fails and the step is retried |
| Attempts per step | 3, counting the first | The run fails |
| Delay between attempts | 30 seconds, then 5 minutes; at most 10 minutes | A longer retry({ delayMs }) is shortened to 10 minutes |
| One continuous unpaused stretch of a run | 4 hours | The sandbox is shut down, stopping any work still running in it |
| Pause duration | No limit; the pause’s timeoutMs if set | The run fails. It does not resume |
| Concurrent runs | No per-organization limit | — |
Events API (POST /v1/workflows/events) | 300 per minute per tenant; 600 per minute per source IP | 429 with Retry-After |
| Unmatched events | 60 per minute per tenant | 429 with Retry-After |
| Webhook trigger URL | 120 per minute per hook; 600 per minute per source IP | 429 with Retry-After |
| Request body | Events API 256 KiB; webhook trigger 1 MiB | Request rejected |
The sections below explain each limit.
Run and step duration
Section titled “Run and step duration”A run has no overall time limit. A run that keeps moving between steps, or waits in a pause, can last as long as it needs to.
Each step attempt has a deadline. The default is 15 minutes. Set timeoutMs on the step to give it a different deadline:
const render = defineStep({ name: "render", next: ["publish"], timeoutMs: 30 * 60 * 1000, // 30 minutes async run(input, ctx) { // ... },});When an attempt passes its deadline, it is recorded as a failed attempt and the step is retried. The runtime checks deadlines once a minute, so a timed-out attempt can be failed up to about a minute after its deadline.
A step runs in a sandbox. The sandbox is created with a 4-hour lifetime that is never extended, and the steps of a run reuse it until the run pauses. A pause releases the sandbox, and the resume starts a fresh one with a fresh 4 hours. Keep any continuous stretch of work between pauses under 4 hours. For longer work, launch it and pause until it finishes; see Long-running steps: pause & resume.
Retries
Section titled “Retries”A step gets 3 attempts in total, counting the first. An attempt is used up when the step throws, times out, or returns retry(...). After the third failed attempt, the run fails.
Between attempts the runtime waits 30 seconds before the second attempt and 5 minutes before the third. A step that returns retry({ delayMs }) sets its own wait instead, capped at 10 minutes.
Pauses
Section titled “Pauses”A pause has no maximum duration. A pauseUntilSignal(...) without timeoutMs waits until a matching signal arrives, for as long as that takes. A paused run releases its sandbox about a minute after pausing and uses no compute while it waits.
With timeoutMs, the pause records a deadline. If no matching signal arrives by then, the run fails with a pause-timeout error (PauseTimeoutError). The runtime does not resume the run, does not call the resume step, and does not supply a fallback payload. The check runs once a minute, so the failure lands up to about a minute after the deadline. A signal sent after the run has failed matches nothing and returns { "matched": 0 }.
Failed runs, including pause timeouts, emit sapiom.run.failed, so a watchdog agent can react to them.
Concurrency
Section titled “Concurrency”Sapiom does not limit how many runs an organization has in progress at once. Runs from every tenant share the runtime’s worker capacity, so under heavy load a run can wait in a queue before its next step starts. Paused runs do not use worker capacity.
Event and webhook triggers start runs through the same shared queue. The ingress rate limits below bound how quickly one tenant or one hook can add to it.
Event and webhook ingress
Section titled “Event and webhook ingress”Both ingress routes are rate limited over a one-minute window. A rejected request answers 429 with a Retry-After header in seconds. Back off for that long and resend with the same event id (events API) or the same X-Sapiom-Event-Id (webhooks); a repeat of an accepted delivery is deduplicated, so the retry is safe.
| Route | Limit | Scope |
|---|---|---|
POST /v1/workflows/events | 300 requests per minute | Per tenant |
| 60 unmatched events per minute | Per tenant. Counts events that no active trigger subscribes to | |
| 600 requests per minute | Per source IP | |
POST to a webhook trigger URL | 120 requests per minute | Per hook URL |
| 600 requests per minute | Per source IP |
The per-IP limit is checked first. Every process behind one NAT egress address shares it.
Body size limits: the events API accepts up to 256 KiB per request, and a webhook trigger URL accepts up to 1 MiB.
If the webhook rate limiter itself is unavailable, the hook URL answers 503 with Retry-After: 5 instead of accepting the delivery unchecked. Retry it the same way.
See Triggers for the request formats, signing, and receipts.
© 2026 Sapiom, Inc.