Skip to content
3 changes: 3 additions & 0 deletions docs/design-patterns/activity-dependency-injection.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
id: activity-dependency-injection
title: "Activity Dependency Injection"
description: "Injects external dependencies into Activities at Worker startup, keeping Workflow code deterministic and Activities testable."
tags:
- Design Patterns
- Workers
---

import Tabs from '@theme/Tabs';
Expand Down
3 changes: 3 additions & 0 deletions docs/design-patterns/approval.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ id: approval
title: "Approval Pattern"
sidebar_label: "Approval"
description: "Human-in-the-loop Workflows that block until external approval decisions are made. Uses Signals to capture approval data with metadata."
tags:
- Design Patterns
- Signals
---

import Tabs from '@theme/Tabs';
Expand Down
3 changes: 3 additions & 0 deletions docs/design-patterns/batch-iterator.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
id: batch-iterator
title: "Batch Iterator"
description: "Pages through unbounded datasets using Continue-As-New to prevent history overflow while maintaining exactly-once processing guarantees."
tags:
- Design Patterns
- Workflows
---

import Tabs from '@theme/Tabs';
Expand Down
6 changes: 4 additions & 2 deletions docs/design-patterns/batch-processing-patterns.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
id: batch-processing-patterns
title: "Batch Processing Patterns"
description: "Compare Fan-Out, Batch Iterator, Sliding Window, and MapReduce Tree patterns for processing large record sets reliably at scale."
tags:
- Design Patterns
---

import PatternCards from '@site/src/components/PatternCards';
Expand All @@ -13,7 +15,7 @@ These patterns process large volumes of records reliably, at scale, and without
| Pattern | Record set size | Parallelism model | Workflow-based rate control |
|---|---|---|---|
| [Basic Workflow](#basic-workflow-single-tier-fan-out) | Small (up to a few hundred records) | Sequential or parallel activities in one Workflow | No |
| [Fan-Out with Child Workflows](/design-patterns/fanout-child-workflows) | Up to ~4M records | Fixed concurrency (one child per chunk) | No |
| [Fan-Out with Child Workflows](/design-patterns/fanout-child-workflows) | Up to ~500K records | Fixed concurrency (one child per chunk) | No |
| [Batch Iterator](/design-patterns/batch-iterator) | Unlimited | Limited (activities per page) | Yes — fixed page rate |
| [Sliding Window](/design-patterns/sliding-window) | Unlimited | Bounded window of concurrent children | Yes — configurable window |
| [MapReduce Tree](/design-patterns/mapreduce-tree) | Unlimited | Fully parallel recursive tree | No — maximum speed |
Expand All @@ -25,7 +27,7 @@ These patterns process large volumes of records reliably, at scale, and without
href: "/design-patterns/fanout-child-workflows",
icon: "fanout-child-workflows-icon.svg",
title: "Fan-Out with Child Workflows",
description: "Splits a record set into fixed-size chunks and assigns each to an independent child Workflow. Direct to reason about; best for record sets up to ~4M items.",
description: "Splits a record set into fixed-size chunks and assigns each to an independent child Workflow. Direct to reason about; best for record sets up to ~500K items.",
},
{
href: "/design-patterns/batch-iterator",
Expand Down
5 changes: 4 additions & 1 deletion docs/design-patterns/child-workflows.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ id: child-workflows
title: "Child Workflows Pattern"
sidebar_label: "Child Workflows"
description: "Decomposes complex Workflows into smaller, reusable units. Each child has an independent Workflow ID, history, and lifecycle."
tags:
- Design Patterns
- Child Workflows
---

import Tabs from '@theme/Tabs';
Expand Down Expand Up @@ -764,7 +767,7 @@ Starting a Child Workflow has more overhead than starting an Activity.
## Common pitfalls

- **Treating Child Workflows like Activities.** Child Workflows are for orchestration, not for executing external code. If you only need to call an API or run a function, use an Activity instead.
- **Spawning unbounded children in a loop.** Starting thousands of Child Workflows without batching can overwhelm the Temporal Service and bloat the parent's event history. Use fixed-size batches or a sliding window.
- **Spawning unbounded children in a loop.** Starting thousands of Child Workflows without batching can overwhelm the Temporal Service and bloat the parent's event history. Temporal enforces a hard limit of 2,000 pending (in-flight) children per parent, but the [recommended cap](/child-workflows#when-to-use-child-workflows) is lower: a single parent should not spawn more than 1,000 Child Workflow Executions in total, since each one adds more history to the parent than an Activity would. Use fixed-size batches or a sliding window.
- **Ignoring the Parent Close Policy.** The default policy is TERMINATE, which kills children when the parent closes. If children must outlive the parent, set the policy to ABANDON explicitly.
- **Using synchronous calls when async is needed.** Calling a Child Workflow synchronously blocks the parent until the child completes. For long-running children, use the async API (`Async.function()` in Java, `startChild()` in TypeScript, `start_child_workflow()` in Python, or collect Futures without calling `.Get()` in Go) to avoid stalling the parent.
- **Omitting Workflow IDs.** Without explicit Workflow IDs, you lose the ability to deduplicate or look up Child Workflows by a meaningful identifier. Generate deterministic IDs based on business keys.
Expand Down
3 changes: 3 additions & 0 deletions docs/design-patterns/continue-as-new.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ id: continue-as-new
title: "Continue-As-New Pattern"
sidebar_label: "Continue-As-New"
description: "Prevents unbounded history growth by completing the current execution and starting a new one with fresh history."
tags:
- Design Patterns
- Workflows
---

import Tabs from '@theme/Tabs';
Expand Down
3 changes: 3 additions & 0 deletions docs/design-patterns/delayed-callback.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ id: delayed-callback
title: "Delayed Callback (Webhooks)"
sidebar_label: "Delayed Callback"
description: "Webhooks become durable: inbound calls arrive as Signals, outbound calls fire after a durable sleep, and Activities complete later via task tokens."
tags:
- Design Patterns
- Signals
---

import Tabs from '@theme/Tabs';
Expand Down
156 changes: 156 additions & 0 deletions docs/design-patterns/delayed-retry.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
id: delayed-retry
title: "Delayed Retry"
description: "Override one failure's retry interval with nextRetryDelay on ApplicationFailure, matching the wait time the error reports."
tags:
- Design Patterns
- Errors
---

import Tabs from '@theme/Tabs';
Expand Down Expand Up @@ -65,6 +68,61 @@ Extract the wait duration from the error or response and pass it to `Application
The RetryPolicy's `MaximumAttempts` and `ScheduleToCloseTimeout` still apply — only the interval for the next retry is overridden.

<Tabs groupId="language" queryString>
<TabItem value="python" label="Python">

```python
# activities.py
from datetime import timedelta
from temporalio import activity
from temporalio.exceptions import ApplicationError

@activity.defn
async def call_api(endpoint: str) -> str:
response = await http_client.get(endpoint)

if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
if retry_after is not None:
raise ApplicationError(
f"Rate limited — retrying after {retry_after}s",
type="RateLimitError",
next_retry_delay=timedelta(seconds=int(retry_after)),
)
raise ApplicationError(
"Rate limited — retrying per RetryPolicy", type="RateLimitError"
)

return response.text
```

</TabItem>
<TabItem value="go" label="Go">

```go
// rate_limited_activity.go
func CallApi(ctx context.Context, endpoint string) (string, error) {
response, err := httpClient.Get(endpoint)
if err != nil {
return "", err
}

if response.StatusCode == 429 {
if retryAfter := response.Header.Get("Retry-After"); retryAfter != "" {
seconds, _ := strconv.Atoi(retryAfter)
return "", temporal.NewApplicationErrorWithOptions(
fmt.Sprintf("Rate limited — retrying after %ds", seconds),
"RateLimitError",
temporal.ApplicationErrorOptions{NextRetryDelay: time.Duration(seconds) * time.Second},
)
}
return "", temporal.NewApplicationError("Rate limited — retrying per RetryPolicy", "RateLimitError")
}

return response.Body, nil
}
```

</TabItem>
<TabItem value="java" label="Java">

```java
Expand Down Expand Up @@ -131,6 +189,54 @@ export async function callApi(endpoint: string): Promise<string> {
You can also set the delay dynamically based on the attempt number — for example, to implement a custom backoff that differs from exponential, or to add a known base delay on top of the standard backoff.

<Tabs groupId="language" queryString>
<TabItem value="python" label="Python">

```python
# activities.py
from datetime import timedelta
from temporalio import activity
from temporalio.exceptions import ApplicationError

@activity.defn
async def process(input: str) -> str:
attempt = activity.info().attempt

try:
return await downstream_service.call(input)
except ServiceUnavailableError as e:
# Custom delay: 3 seconds × attempt number (3s, 6s, 9s, …)
raise ApplicationError(
f"Service unavailable on attempt {attempt}",
type="ServiceUnavailable",
next_retry_delay=timedelta(seconds=3 * attempt),
) from e
```

</TabItem>
<TabItem value="go" label="Go">

```go
// backoff_activity.go
func Process(ctx context.Context, input string) (string, error) {
attempt := activity.GetInfo(ctx).Attempt

result, err := downstreamService.Call(input)
if err != nil {
// Custom delay: 3 seconds × attempt number (3s, 6s, 9s, …)
return "", temporal.NewApplicationErrorWithOptions(
fmt.Sprintf("Service unavailable on attempt %d", attempt),
"ServiceUnavailable",
temporal.ApplicationErrorOptions{
Cause: err,
NextRetryDelay: 3 * time.Second * time.Duration(attempt),
},
)
}
return result, nil
}
```

</TabItem>
<TabItem value="java" label="Java">

```java
Expand Down Expand Up @@ -192,6 +298,56 @@ The Workflow sets a normal `RetryPolicy`.
The `nextRetryDelay` set in the Activity overrides the interval only for the retry following that specific failure — subsequent attempts fall back to the RetryPolicy schedule if `nextRetryDelay` is not set again.

<Tabs groupId="language" queryString>
<TabItem value="python" label="Python">

```python
# workflows.py
from datetime import timedelta
from temporalio import workflow
from temporalio.common import RetryPolicy

with workflow.unsafe.imports_passed_through():
from activities import call_api

@workflow.defn
class ApiWorkflow:
@workflow.run
async def run(self, endpoint: str) -> str:
return await workflow.execute_activity(
call_api,
endpoint,
start_to_close_timeout=timedelta(seconds=10),
retry_policy=RetryPolicy(
initial_interval=timedelta(seconds=1),
backoff_coefficient=2.0,
maximum_attempts=10,
),
)
```

</TabItem>
<TabItem value="go" label="Go">

```go
// api_workflow.go
func ApiWorkflow(ctx workflow.Context, endpoint string) (string, error) {
ao := workflow.ActivityOptions{
StartToCloseTimeout: 10 * time.Second,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: time.Second,
BackoffCoefficient: 2.0,
MaximumAttempts: 10,
},
}
ctx = workflow.WithActivityOptions(ctx, ao)

var result string
err := workflow.ExecuteActivity(ctx, CallApi, endpoint).Get(ctx, &result)
return result, err
}
```

</TabItem>
<TabItem value="java" label="Java">

```java
Expand Down
3 changes: 3 additions & 0 deletions docs/design-patterns/delayed-start.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ id: delayed-start
title: "Delayed Start Pattern"
sidebar_label: "Delayed Start"
description: "Creates Workflows immediately but defers execution until a specified delay expires. Fits one-time scheduled operations and grace periods."
tags:
- Design Patterns
- Workflows
---

import Tabs from '@theme/Tabs';
Expand Down
2 changes: 2 additions & 0 deletions docs/design-patterns/distributed-transaction-patterns.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
id: distributed-transaction-patterns
title: "Distributed Transaction Patterns"
description: "Pattern selection guide for distributed transactions, with a decision tree for choosing between Saga and Early Return."
tags:
- Design Patterns
---

import PatternCards from '@site/src/components/PatternCards';
Expand Down
4 changes: 4 additions & 0 deletions docs/design-patterns/downstream-rate-limiting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
id: downstream-rate-limiting
title: "Downstream Rate Limiting"
description: "Rate-limits calls to a downstream service by routing throttled Activities to a dedicated Task Queue with a server-enforced throughput cap."
tags:
- Design Patterns
- Task Queues
---

import Tabs from '@theme/Tabs';
Expand Down Expand Up @@ -279,6 +282,7 @@ The concurrency slots (`MaxConcurrentActivityExecutionSize`, `MaxConcurrentWorkf
- **Confusing throughput limits with concurrency limits.** `MaxTaskQueueActivitiesPerSecond` controls starts per second; `MaxConcurrentActivityExecutionSize` controls simultaneous executions. Long-running Activities that hold slots for minutes may exhaust concurrency before the RPS cap applies.
- **Setting the cap far below actual demand.** A cap much lower than actual submission rate causes the queue to grow unboundedly. Monitor queue depth and raise the cap or add more Workers when throughput requirements grow.
- **Expecting a perfectly even per-second rate.** The limit is enforced across the queue's partitions, default four. The server maintains the configured rate as an average over time but can dispatch a short burst above it, up to roughly the rate divided across partitions. If the downstream service rejects any momentary overshoot, set the cap below the hard limit to leave headroom, or reduce the partition count for the queue.
- **Eager Activity execution bypassing the rate-limited queue.** [Eager Activity Start](/develop/worker-performance#eager-activity-start) lets the server hand an Activity straight back to the Worker that just completed the scheduling Workflow Task, skipping the Task Queue and its rate limit entirely. In Python, disable it explicitly with `disable_eager_activity_execution=True` on the `Worker`. The Go SDK disables it automatically whenever `TaskQueueActivitiesPerSecond` is set, so no separate flag is needed there — but confirm the equivalent for your SDK before relying on the queue-level cap alone.

## Related

Expand Down
Loading