|
| 1 | +--- |
| 2 | +sidebar_position: 5 |
| 3 | +--- |
| 4 | + |
| 5 | +# Structural Limits |
| 6 | + |
| 7 | +Structural limits cap the resource consumption of a single workflow run. When an operation would exceed a configured limit, the engine records a typed failure with a machine-readable `structural_limit` failure category and the specific limit kind, then fails the run. This protects the system from unbounded fan-out, oversized payloads, and metadata bloat. |
| 8 | + |
| 9 | +## Limit kinds |
| 10 | + |
| 11 | +| Limit kind | Default | What it caps | |
| 12 | +|---|---|---| |
| 13 | +| `pending_activity_count` | 2,000 | Non-terminal activity executions open simultaneously | |
| 14 | +| `pending_child_count` | 1,000 | Non-terminal child workflows open simultaneously | |
| 15 | +| `pending_timer_count` | 2,000 | Pending timers open simultaneously | |
| 16 | +| `pending_signal_count` | 5,000 | Unprocessed signals pending simultaneously | |
| 17 | +| `pending_update_count` | 500 | Unresolved updates pending simultaneously | |
| 18 | +| `command_batch_size` | 1,000 | Items in a single parallel fan-out (`all()` / `parallel()`) | |
| 19 | +| `payload_size_bytes` | 2 MiB | Serialized size of a single argument payload | |
| 20 | +| `memo_size_bytes` | 256 KiB | Serialized size of non-indexed memo metadata | |
| 21 | +| `search_attribute_size_bytes` | 40 KiB | Serialized size of indexed search-attribute metadata | |
| 22 | + |
| 23 | +All limits are enforced at the point of scheduling or recording. A value of `0` disables the check for that limit kind. |
| 24 | + |
| 25 | +## Configuration |
| 26 | + |
| 27 | +Override any limit through `workflows.v2.structural_limits` in your config or via environment variables: |
| 28 | + |
| 29 | +```php |
| 30 | +// config/workflows.php |
| 31 | +'v2' => [ |
| 32 | + 'structural_limits' => [ |
| 33 | + 'pending_activity_count' => (int) env('WORKFLOW_V2_LIMIT_PENDING_ACTIVITIES', 2000), |
| 34 | + 'pending_child_count' => (int) env('WORKFLOW_V2_LIMIT_PENDING_CHILDREN', 1000), |
| 35 | + 'pending_timer_count' => (int) env('WORKFLOW_V2_LIMIT_PENDING_TIMERS', 2000), |
| 36 | + 'pending_signal_count' => (int) env('WORKFLOW_V2_LIMIT_PENDING_SIGNALS', 5000), |
| 37 | + 'pending_update_count' => (int) env('WORKFLOW_V2_LIMIT_PENDING_UPDATES', 500), |
| 38 | + 'command_batch_size' => (int) env('WORKFLOW_V2_LIMIT_COMMAND_BATCH_SIZE', 1000), |
| 39 | + 'payload_size_bytes' => (int) env('WORKFLOW_V2_LIMIT_PAYLOAD_SIZE_BYTES', 2097152), |
| 40 | + 'memo_size_bytes' => (int) env('WORKFLOW_V2_LIMIT_MEMO_SIZE_BYTES', 262144), |
| 41 | + 'search_attribute_size_bytes' => (int) env('WORKFLOW_V2_LIMIT_SEARCH_ATTRIBUTE_SIZE_BYTES', 40960), |
| 42 | + ], |
| 43 | +], |
| 44 | +``` |
| 45 | + |
| 46 | +## Enforcement points |
| 47 | + |
| 48 | +### Pending count limits |
| 49 | + |
| 50 | +Before the executor schedules an activity, child workflow, or timer, it counts the currently non-terminal items of that type on the run. If the count is already at or above the configured limit, the run fails immediately with a `StructuralLimitExceededException`. |
| 51 | + |
| 52 | +This protects against patterns like unbounded parallel fan-out loops that accumulate thousands of pending operations: |
| 53 | + |
| 54 | +```php |
| 55 | +// This will fail if $items exceeds the pending_activity_count limit |
| 56 | +$calls = []; |
| 57 | +foreach ($items as $item) { |
| 58 | + $calls[] = startActivity(ProcessItemActivity::class, $item); |
| 59 | +} |
| 60 | +return all($calls); // Also checked against command_batch_size |
| 61 | +``` |
| 62 | + |
| 63 | +To handle large batches within the limits, process items in bounded chunks: |
| 64 | + |
| 65 | +```php |
| 66 | +foreach (array_chunk($items, 500) as $chunk) { |
| 67 | + $calls = []; |
| 68 | + foreach ($chunk as $item) { |
| 69 | + $calls[] = startActivity(ProcessItemActivity::class, $item); |
| 70 | + } |
| 71 | + all($calls); |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +### Command batch size |
| 76 | + |
| 77 | +The `all()` and `parallel()` functions check the total number of leaf operations in a single fan-out group against `command_batch_size`. This is checked before any individual activities or children are scheduled, so the run fails cleanly rather than partially scheduling a batch. |
| 78 | + |
| 79 | +### Payload size limits |
| 80 | + |
| 81 | +Payload size checks apply to serialized argument data. When a serialized payload exceeds the configured `payload_size_bytes`, the engine rejects the operation. |
| 82 | + |
| 83 | +### Metadata size limits |
| 84 | + |
| 85 | +Memo and search-attribute metadata are checked against `memo_size_bytes` and `search_attribute_size_bytes` respectively when upserting metadata. |
| 86 | + |
| 87 | +## Failure taxonomy |
| 88 | + |
| 89 | +When a structural limit is exceeded, the engine records: |
| 90 | + |
| 91 | +- A `WorkflowFailure` row with `failure_category = structural_limit` |
| 92 | +- A `WorkflowFailed` history event with: |
| 93 | + - `failure_category = structural_limit` |
| 94 | + - `structural_limit_kind` — the specific limit that was exceeded (e.g. `pending_activity_count`, `command_batch_size`) |
| 95 | + - `structural_limit_value` — the current count or size that triggered the limit |
| 96 | + - `structural_limit_configured` — the configured ceiling |
| 97 | + |
| 98 | +This metadata is machine-readable, so operators, Waterline, and external tooling can identify the root cause without parsing free-text messages. |
| 99 | + |
| 100 | +## Health check |
| 101 | + |
| 102 | +The current structural limits configuration is included in the v2 health check snapshot under `structural_limits`, making the active ceilings visible to operators: |
| 103 | + |
| 104 | +```json |
| 105 | +{ |
| 106 | + "structural_limits": { |
| 107 | + "pending_activity_count": 2000, |
| 108 | + "pending_child_count": 1000, |
| 109 | + "pending_timer_count": 2000, |
| 110 | + "pending_signal_count": 5000, |
| 111 | + "pending_update_count": 500, |
| 112 | + "command_batch_size": 1000, |
| 113 | + "payload_size_bytes": 2097152, |
| 114 | + "memo_size_bytes": 262144, |
| 115 | + "search_attribute_size_bytes": 40960 |
| 116 | + } |
| 117 | +} |
| 118 | +``` |
| 119 | + |
| 120 | +## Waterline |
| 121 | + |
| 122 | +Waterline surfaces structural-limit failures in the exceptions table with the `structural_limit` failure category. The timeline failure details include the limit kind, current value, and configured ceiling. |
0 commit comments