diff --git a/docs/design-patterns/activity-dependency-injection.mdx b/docs/design-patterns/activity-dependency-injection.mdx index e84278aad4..66bb1ee76d 100644 --- a/docs/design-patterns/activity-dependency-injection.mdx +++ b/docs/design-patterns/activity-dependency-injection.mdx @@ -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'; diff --git a/docs/design-patterns/approval.mdx b/docs/design-patterns/approval.mdx index 3d99bb8ff2..260ac6f060 100644 --- a/docs/design-patterns/approval.mdx +++ b/docs/design-patterns/approval.mdx @@ -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'; diff --git a/docs/design-patterns/batch-iterator.mdx b/docs/design-patterns/batch-iterator.mdx index f1a150f411..18f1290962 100644 --- a/docs/design-patterns/batch-iterator.mdx +++ b/docs/design-patterns/batch-iterator.mdx @@ -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'; diff --git a/docs/design-patterns/batch-processing-patterns.mdx b/docs/design-patterns/batch-processing-patterns.mdx index d75f4b13b8..c504b4f1a0 100644 --- a/docs/design-patterns/batch-processing-patterns.mdx +++ b/docs/design-patterns/batch-processing-patterns.mdx @@ -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'; @@ -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 | @@ -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", diff --git a/docs/design-patterns/child-workflows.mdx b/docs/design-patterns/child-workflows.mdx index dd1e28aa0e..a72d3788c3 100644 --- a/docs/design-patterns/child-workflows.mdx +++ b/docs/design-patterns/child-workflows.mdx @@ -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'; @@ -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. diff --git a/docs/design-patterns/continue-as-new.mdx b/docs/design-patterns/continue-as-new.mdx index d9397d0a34..85337100a5 100644 --- a/docs/design-patterns/continue-as-new.mdx +++ b/docs/design-patterns/continue-as-new.mdx @@ -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'; diff --git a/docs/design-patterns/delayed-callback.mdx b/docs/design-patterns/delayed-callback.mdx index eae108e562..d7b030bcb2 100644 --- a/docs/design-patterns/delayed-callback.mdx +++ b/docs/design-patterns/delayed-callback.mdx @@ -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'; diff --git a/docs/design-patterns/delayed-retry.mdx b/docs/design-patterns/delayed-retry.mdx index 46cd32f407..55b31f9b0b 100644 --- a/docs/design-patterns/delayed-retry.mdx +++ b/docs/design-patterns/delayed-retry.mdx @@ -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'; @@ -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. + + +```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 +``` + + + + +```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 +} +``` + + ```java @@ -131,6 +189,54 @@ export async function callApi(endpoint: string): Promise { 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. + + +```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 +``` + + + + +```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 +} +``` + + ```java @@ -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. + + +```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, + ), + ) +``` + + + + +```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 +} +``` + + ```java diff --git a/docs/design-patterns/delayed-start.mdx b/docs/design-patterns/delayed-start.mdx index 9dc89a5a39..8bfc973af7 100644 --- a/docs/design-patterns/delayed-start.mdx +++ b/docs/design-patterns/delayed-start.mdx @@ -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'; diff --git a/docs/design-patterns/distributed-transaction-patterns.mdx b/docs/design-patterns/distributed-transaction-patterns.mdx index 31b4acfa8b..e36e15a527 100644 --- a/docs/design-patterns/distributed-transaction-patterns.mdx +++ b/docs/design-patterns/distributed-transaction-patterns.mdx @@ -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'; diff --git a/docs/design-patterns/downstream-rate-limiting.mdx b/docs/design-patterns/downstream-rate-limiting.mdx index d8491bebf4..a88ef9c44e 100644 --- a/docs/design-patterns/downstream-rate-limiting.mdx +++ b/docs/design-patterns/downstream-rate-limiting.mdx @@ -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'; @@ -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 diff --git a/docs/design-patterns/eager-workflow-start.mdx b/docs/design-patterns/eager-workflow-start.mdx index e115be31c2..f95821ac23 100644 --- a/docs/design-patterns/eager-workflow-start.mdx +++ b/docs/design-patterns/eager-workflow-start.mdx @@ -2,13 +2,16 @@ id: eager-workflow-start title: "Eager Workflow Start" description: "Eager Workflow Start sends the first Workflow Task directly to a co-located Worker, skipping the Matching Service to cut startup latency." +tags: + - Design Patterns + - Workers --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; :::info[TLDR] -**Bypass the Temporal Matching Service by dispatching the first Workflow Task directly to a co-located Worker.** The Worker and the client that starts the Workflow must share the same process and server connection. Eager Workflow Start eliminates the Matching Service round-trip, saving approximately 30–50 ms per Workflow start. When combined with Local Activities, this pattern achieves ~265 ms total-workflow latency (vs ~850 ms baseline). The TypeScript SDK does not support Eager Workflow Start. +**Bypass the Temporal Matching Service by dispatching the first Workflow Task directly to a co-located Worker.** The Worker and the client that starts the Workflow must share the same process and server connection. Eager Workflow Start eliminates the Matching Service round-trip, saving approximately 30–50 ms per Workflow start. When combined with Local Activities, this pattern achieves ~265 ms total-workflow latency (vs ~850 ms baseline). Supported by the Go, Java, Python, TypeScript, and .NET SDKs. ::: ## Overview @@ -53,17 +56,9 @@ For applications where the starter and Worker share the same deployment unit—s ## Solution -Start a Worker in the same process as the workflow starter, using the same client connection. Set `EnableEagerStart: true` (Go), `setDisableEagerExecution(false)` (Java), or `request_eager_start=True` (Python) on the `StartWorkflowOptions`. The SDK signals to the server that a local Worker is available, and the server returns the first Workflow Task inline. +Start a Worker in the same process as the workflow starter, using the same client connection. Set `EnableEagerStart: true` (Go), `setDisableEagerExecution(false)` (Java), `request_eager_start=True` (Python), `requestEagerStart: true` (TypeScript, on a `NativeConnection` shared between Worker and Client), or `RequestEagerStart` (.NET) on the workflow start options. The SDK signals to the server that a local Worker is available, and the server returns the first Workflow Task inline. -:::warning[Feature flag for self-hosted Temporal] -On self-hosted Temporal Server, Eager Workflow Start may require enabling a dynamic config flag: - -``` ---dynamic-config-value system.enableEagerWorkflowStart=true -``` - -Temporal Cloud and recent versions of the open-source server may enable this by default. Check your server's release notes or documentation to confirm. -::: +Eager Workflow Start is enabled by default in Temporal Cloud and in self-hosted Temporal Server 1.29.0 and later — no additional server configuration or access request is needed. On self-hosted Temporal Server, an operator can disable it with the dynamic config flag `system.enableEagerWorkflowStart` set to `false`; if you don't observe the expected latency improvement, confirm that flag hasn't been turned off. See [Eager Workflow Start](/develop/worker-performance#eager-workflow-start) for the canonical server-side reference. @@ -175,11 +170,53 @@ public class Starter { } ``` + + + +```typescript +// starter.ts — starts the Worker in the same process, then executes the Workflow eagerly +import { NativeConnection, Worker } from '@temporalio/worker'; +import { Client } from '@temporalio/client'; +import { transactionWorkflow } from './workflows'; +import { TASK_QUEUE, TransactionRequest } from './shared'; + +async function run() { + // The Client and the Worker must share this NativeConnection for eager dispatch to work. + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + + const worker = await Worker.create({ + connection, + taskQueue: TASK_QUEUE, + workflowsPath: require.resolve('./workflows'), + activities: { validateTransaction, settleTransaction }, + }); + + const client = new Client({ connection }); + + await worker.runUntil(async () => { + const handle = await client.workflow.start(transactionWorkflow, { + args: [{ amount: 100.0, currency: 'USD' } satisfies TransactionRequest], + workflowId: 'eager-workflow-start-demo', + taskQueue: TASK_QUEUE, + requestEagerStart: true, // Dispatch first WorkflowTask inline + }); + + const result = await handle.result(); + console.log(`Transaction complete: ID=${result.id} Status=${result.status}`); + }); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); +``` + -:::info[TypeScript SDK] -The TypeScript SDK does not currently support Eager Workflow Start. Use [Local Activities](/design-patterns/local-activities) or [Early Return + Local Activities](/design-patterns/early-return-local-activities) for latency-sensitive TypeScript workflows. +:::info[.NET SDK] +The .NET SDK also supports Eager Workflow Start: set `RequestEagerStart = true` on `WorkflowOptions` when starting the Workflow, with the Worker and Client sharing the same connection. ::: ## When to use @@ -188,12 +225,11 @@ The TypeScript SDK does not currently support Eager Workflow Start. Use [Local A - The workflow starter and Worker run in the same deployment unit (for example, a single service that both handles API requests and runs Workers) - You need the absolute minimum total-workflow latency and are already using Local Activities -- The language is Go, Java, or Python +- Any of the Go, Java, Python, TypeScript, or .NET SDKs **Poor fit:** - Workers are deployed independently from starters (the eager request falls back to normal dispatch, which is harmless but provides no benefit) -- You are using the TypeScript SDK - First-response latency matters more than total latency—combine with [Early Return](/design-patterns/early-return) or [Early Return + Local Activities](/design-patterns/early-return-local-activities) for that use case ## Benefits and trade-offs @@ -203,24 +239,24 @@ The TypeScript SDK does not currently support Eager Workflow Start. Use [Local A | Matching Service round-trip | Yes (~30–50 ms) | No (eliminated) | | Worker co-location required | No | Yes (same process + client) | | Fallback behavior | N/A | Graceful fallback to normal dispatch | -| TypeScript SDK support | Yes | No | -| Configuration required | None | `EnableEagerStart`/`request_eager_start`/`setDisableEagerExecution(false)` | -| Self-hosted server flag | N/A | May need `system.enableEagerWorkflowStart=true` | +| SDK support | All | Go, Java, Python, TypeScript, .NET | +| Configuration required | None | `EnableEagerStart`/`request_eager_start`/`setDisableEagerExecution(false)`/`requestEagerStart`/`RequestEagerStart` | +| Self-hosted server flag | N/A | On by default (Server 1.29.0+); disable via `system.enableEagerWorkflowStart=false` | ## Best practices - **Combine with Local Activities.** Eager Workflow Start eliminates the Matching overhead on the first Workflow Task; Local Activities eliminate server round-trips within each Workflow Task. Together they provide the greatest total latency reduction. - **Use a non-blocking Worker start.** Start the Worker before executing the Workflow so it has an available slot. In Go, use `w.Start()` and defer `w.Stop()`. In Python, use `async with Worker(...)`. In Java, call `factory.start()` before creating the workflow stub. - **Do not rely on eager dispatch always firing.** The server falls back to normal dispatch if no local slot is available (for example, the Worker is at capacity). Design the Workflow to work correctly in both cases. -- **Share the same client and connection.** The Worker and the workflow starter must use the same `WorkflowClient` instance (Java), `client.Client` (Go), or `Client` (Python). A Worker using a different connection cannot receive eager tasks from another client. +- **Share the same client and connection.** The Worker and the workflow starter must use the same `WorkflowClient` instance (Java), `client.Client` (Go), `Client` (Python), or `NativeConnection` (TypeScript). A Worker using a different connection cannot receive eager tasks from another client. - **Be mindful of resource sharing in co-located deployments.** When a Worker runs in the same process as a request handler, they share CPU, memory, and failure domains. A spike in activity execution can slow request handling, and vice versa. Monitor Worker CPU, Workflow Task execution latency, and task queue depth to ensure Worker load does not affect client-facing latency. ## Common pitfalls - **Starting the Worker after `ExecuteWorkflow`.** If the Worker is not registered and running before the eager start call, no local slot exists and the request falls back to normal dispatch. - **Expecting eager dispatch in distributed deployments.** If the process that calls `ExecuteWorkflow` is not the same process running the Worker, eager dispatch will never succeed. The call still works, but it provides no latency benefit. -- **Missing the feature flag on self-hosted servers.** If the server dynamic config flag is not set, eager dispatch requests are silently ignored and the execution falls back to normal dispatch. Verify the flag is set if you do not observe the expected latency improvement. -- **Using TypeScript.** The TypeScript SDK does not support Eager Workflow Start. Switch to Python, Go, or Java for this optimization. +- **Assuming the self-hosted server flag is off.** Eager Workflow Start ships enabled by default (Server 1.29.0+). If you don't observe the expected latency improvement, check whether an operator explicitly disabled `system.enableEagerWorkflowStart`, rather than assuming it needs to be turned on. +- **Using a Connection instead of a NativeConnection in TypeScript.** The high-level `Client` and the `Worker` must share a `NativeConnection` object, not just the same server address. A `Worker` created from a separate connection cannot receive eager tasks. ## Related @@ -229,3 +265,7 @@ The TypeScript SDK does not currently support Eager Workflow Start. Use [Local A - [Local Activities](/design-patterns/local-activities) — eliminates per-Activity server round-trips; pairs naturally with Eager Workflow Start - [Early Return + Local Activities](/design-patterns/early-return-local-activities) — minimum first-response latency via Update-with-Start plus Local Activities - [Early Return](/design-patterns/early-return) — returns early to the client via Update-with-Start + +### References + +- [Eager Workflow Start](/develop/worker-performance#eager-workflow-start) — canonical server-side reference, including Eager Activity Start diff --git a/docs/design-patterns/early-return-local-activities.mdx b/docs/design-patterns/early-return-local-activities.mdx index acafbc2e60..751d0190c7 100644 --- a/docs/design-patterns/early-return-local-activities.mdx +++ b/docs/design-patterns/early-return-local-activities.mdx @@ -2,6 +2,9 @@ id: early-return-local-activities title: "Early Return + Local Activities" description: "Extends Early Return by running Phase 1 as Local Activities, so the client's first response comes from in-process work, not a server call." +tags: + - Design Patterns + - Activities --- import Tabs from '@theme/Tabs'; diff --git a/docs/design-patterns/early-return.mdx b/docs/design-patterns/early-return.mdx index 7afbeafa2f..ee48673c95 100644 --- a/docs/design-patterns/early-return.mdx +++ b/docs/design-patterns/early-return.mdx @@ -3,6 +3,9 @@ id: early-return title: "Early Return (Update with Start)" sidebar_label: "Early Return" description: "Synchronous initialization with asynchronous completion. Returns results immediately while processing continues in the background." +tags: + - Design Patterns + - Updates --- import Tabs from '@theme/Tabs'; diff --git a/docs/design-patterns/entity-lifecycle-patterns.mdx b/docs/design-patterns/entity-lifecycle-patterns.mdx index 1763b955ec..aaaf8b8749 100644 --- a/docs/design-patterns/entity-lifecycle-patterns.mdx +++ b/docs/design-patterns/entity-lifecycle-patterns.mdx @@ -2,6 +2,8 @@ id: entity-lifecycle-patterns title: "Entity & Lifecycle Patterns" description: "Pattern selection guide for modeling long-lived stateful entities and managing Workflow history growth over time." +tags: + - Design Patterns --- import PatternCards from '@site/src/components/PatternCards'; diff --git a/docs/design-patterns/entity-workflow.mdx b/docs/design-patterns/entity-workflow.mdx index b2e3528438..56ef844093 100644 --- a/docs/design-patterns/entity-workflow.mdx +++ b/docs/design-patterns/entity-workflow.mdx @@ -3,6 +3,9 @@ id: entity-workflow title: "Entity Workflow Pattern" sidebar_label: "Entity Workflow" description: "A long-lived business entity — a user account, device, or order — gets one Workflow per instance, with Signals and Updates driving every state transition." +tags: + - Design Patterns + - Workflows --- import Tabs from '@theme/Tabs'; diff --git a/docs/design-patterns/error-handling-patterns.mdx b/docs/design-patterns/error-handling-patterns.mdx index bfea76526a..f47efd8bc1 100644 --- a/docs/design-patterns/error-handling-patterns.mdx +++ b/docs/design-patterns/error-handling-patterns.mdx @@ -2,6 +2,8 @@ id: error-handling-patterns title: "Error Handling & Retry Patterns" description: "Pattern selection guide and decision tree for choosing the right retry strategy based on your error type, cost constraints, and recovery requirements." +tags: + - Design Patterns --- import PatternCards from '@site/src/components/PatternCards'; diff --git a/docs/design-patterns/event-accumulator.mdx b/docs/design-patterns/event-accumulator.mdx index 716f51f9f1..73fcfcfd0a 100644 --- a/docs/design-patterns/event-accumulator.mdx +++ b/docs/design-patterns/event-accumulator.mdx @@ -3,6 +3,9 @@ id: event-accumulator title: "Event Accumulator Pattern" sidebar_label: "Event Accumulator" description: "Durably collect and deduplicate signals from multiple senders, then process the batch after a sliding inactivity timeout." +tags: + - Design Patterns + - Signals --- import Tabs from '@theme/Tabs'; diff --git a/docs/design-patterns/external-interaction-patterns.mdx b/docs/design-patterns/external-interaction-patterns.mdx index ebbf4c3da6..9636af2c75 100644 --- a/docs/design-patterns/external-interaction-patterns.mdx +++ b/docs/design-patterns/external-interaction-patterns.mdx @@ -2,6 +2,8 @@ id: external-interaction-patterns title: "External Interaction Patterns" description: "Compares five patterns for waiting on external systems and human decisions: polling, heartbeating Activities, delayed start, webhooks, and approval." +tags: + - Design Patterns --- import PatternCards from '@site/src/components/PatternCards'; diff --git a/docs/design-patterns/fairness.mdx b/docs/design-patterns/fairness.mdx index 90e3e024dd..961d9929c9 100644 --- a/docs/design-patterns/fairness.mdx +++ b/docs/design-patterns/fairness.mdx @@ -2,6 +2,9 @@ id: fairness title: "Fairness" description: "Distributes Worker capacity evenly across tenants or users so that a burst from one caller does not starve the others." +tags: + - Design Patterns + - Task Queues --- import Tabs from '@theme/Tabs'; diff --git a/docs/design-patterns/fanout-child-workflows.mdx b/docs/design-patterns/fanout-child-workflows.mdx index de237f54a0..266dbf35e2 100644 --- a/docs/design-patterns/fanout-child-workflows.mdx +++ b/docs/design-patterns/fanout-child-workflows.mdx @@ -2,6 +2,9 @@ id: fanout-child-workflows title: "Fan-Out with Child Workflows" description: "Distributes a large record set across parallel Child Workflows for concurrent processing with automatic scaling." +tags: + - Design Patterns + - Child Workflows --- import Tabs from '@theme/Tabs'; @@ -312,7 +315,7 @@ public class RecordBatchWorkflowImpl implements RecordBatchWorkflow { ## Common pitfalls -- **Starting too many children at once.** Each child start adds to the parent's history. Temporal enforces a default limit of 2,000 pending (in-flight) child Workflows per parent; keep well under it. See [Temporal guidance](/child-workflows#when-to-use-child-workflows). If you need more children, switch to [MapReduce Tree](/design-patterns/mapreduce-tree) or [Sliding Window](/design-patterns/sliding-window). +- **Starting too many children at once.** Each child start adds to the parent's history. Temporal enforces a default limit of 2,000 pending (in-flight) child Workflows per parent, but the recommended cap is lower still: a single parent should not spawn more than 1,000 Child Workflow Executions, since each one adds more history to the parent than an Activity would. See [Temporal guidance](/child-workflows#when-to-use-child-workflows). If you need more children, switch to [MapReduce Tree](/design-patterns/mapreduce-tree) or [Sliding Window](/design-patterns/sliding-window). - **Passing large lists of IDs.** Workflow inputs are stored in event history. Passing millions of record IDs as a list will blow the history size limit. Use offset + length instead. - **Ignoring child failures.** A failed child does not automatically fail the parent unless you await all results. Always await child handles and handle errors explicitly. diff --git a/docs/design-patterns/fast-slow-retries.mdx b/docs/design-patterns/fast-slow-retries.mdx index bd50618c9d..e970a970a1 100644 --- a/docs/design-patterns/fast-slow-retries.mdx +++ b/docs/design-patterns/fast-slow-retries.mdx @@ -2,6 +2,9 @@ id: fast-slow-retries title: "Fast/Slow Retries" description: "Retry fast with a short interval first, then shift to a slow, unlimited interval so the Workflow outlasts an extended downstream outage." +tags: + - Design Patterns + - Errors --- import Tabs from '@theme/Tabs'; diff --git a/docs/design-patterns/fixed-count-retries.mdx b/docs/design-patterns/fixed-count-retries.mdx index be63ecd6c5..058c4a90ce 100644 --- a/docs/design-patterns/fixed-count-retries.mdx +++ b/docs/design-patterns/fixed-count-retries.mdx @@ -2,6 +2,9 @@ id: fixed-count-retries title: "Fixed Count of Retries" description: "Cap the number of Activity retry attempts to control cost when each attempt consumes a paid or limited resource." +tags: + - Design Patterns + - Errors --- import Tabs from '@theme/Tabs'; diff --git a/docs/design-patterns/fixed-wall-time-retries.mdx b/docs/design-patterns/fixed-wall-time-retries.mdx index af18ef2f5f..5d5122a434 100644 --- a/docs/design-patterns/fixed-wall-time-retries.mdx +++ b/docs/design-patterns/fixed-wall-time-retries.mdx @@ -2,6 +2,9 @@ id: fixed-wall-time-retries title: "Fixed Wall-Time Retries" description: "Bound the total elapsed time across all retry attempts to enforce a business SLA, regardless of how many individual attempts occur." +tags: + - Design Patterns + - Errors --- import Tabs from '@theme/Tabs'; diff --git a/docs/design-patterns/index.mdx b/docs/design-patterns/index.mdx index 0dbb5ca1cb..dd2893d78f 100644 --- a/docs/design-patterns/index.mdx +++ b/docs/design-patterns/index.mdx @@ -3,6 +3,8 @@ id: index title: "Temporal Design Patterns" sidebar_label: "Design Patterns" description: "A catalog of common, reusable, and proven design patterns for Temporal Workflows, organized by problem domain." +tags: + - Design Patterns slug: /design-patterns --- diff --git a/docs/design-patterns/local-activities.mdx b/docs/design-patterns/local-activities.mdx index 00ee54516a..cffcecc088 100644 --- a/docs/design-patterns/local-activities.mdx +++ b/docs/design-patterns/local-activities.mdx @@ -2,6 +2,9 @@ id: local-activities title: "Local Activities" description: "Local Activities run inside the Worker process, skipping server round-trips for short, idempotent Activities on a latency-sensitive path." +tags: + - Design Patterns + - Activities --- import Tabs from '@theme/Tabs'; @@ -193,7 +196,7 @@ public class Impl implements TransactionWorkflow { ## Common pitfalls -- **Exceeding the Workflow Task timeout.** If a Local Activity takes longer than the Workflow Task timeout (default 10 seconds), the entire task times out and retries—including any Local Activities that already completed in memory during that task. +- **Exceeding the Workflow Task timeout.** If a Local Activity takes longer than the Workflow Task timeout (default 10 seconds) and Workflow Task heartbeating doesn't cover it, the entire task times out and retries—including any Local Activities that already completed in memory during that task. The SDK mitigates this automatically: once a running Local Activity passes about 80% of the Workflow Task timeout, the Worker heartbeats by completing the current Workflow Task and requesting a new one, so a single long Local Activity (or a chain of them) can run past one Workflow Task's timeout without the whole task failing. Heartbeating adds Events to history and delays processing of incoming Signals until the Local Activity finishes — see [Local Activity](/local-activity#workflow-task-heartbeating) for the full mechanism. - **Assuming exactly-once semantics.** Unlike regular Activities, a Local Activity does not get its own persisted history event until the Workflow Task completes. A crashed Worker causes the whole task to re-run. This compounds when Local Activities are chained: if a Worker crashes after the third of five sequential Local Activities, all five re-execute on the next attempt. If you need a durable checkpoint between each step, use regular Activities instead. - **Long retry intervals.** Each retry attempt with back-off creates a server-side timer event. For truly short Activities, use a tight `scheduleToCloseTimeout` and allow immediate retries rather than spaced-out back-off. @@ -205,3 +208,7 @@ public class Impl implements TransactionWorkflow { - [Early Return](/design-patterns/early-return) — returns a response to the caller before the Workflow finishes, independent of Local Activities - [Eager Workflow Start](/design-patterns/eager-workflow-start) — eliminates the server Matching step when starting a Workflow for additional latency reduction - [Long Running Activity](/design-patterns/long-running-activity) — the right choice when Activities need heartbeating and long execution windows + +### References + +- [Local Activity](/local-activity) — canonical concept reference, including durability guarantees and Workflow Task heartbeating diff --git a/docs/design-patterns/long-running-activity.mdx b/docs/design-patterns/long-running-activity.mdx index c5b441049e..831e0e280e 100644 --- a/docs/design-patterns/long-running-activity.mdx +++ b/docs/design-patterns/long-running-activity.mdx @@ -3,6 +3,9 @@ id: long-running-activity title: "Long-Running Activity - Tracking Progress and Handling Cancellation with Heartbeats" sidebar_label: "Long Running Activity" description: "Long-running Activities report progress via heartbeats and enable resumption after failures with cancellation support." +tags: + - Design Patterns + - Activities --- import Tabs from '@theme/Tabs'; diff --git a/docs/design-patterns/mapreduce-tree.mdx b/docs/design-patterns/mapreduce-tree.mdx index d065d5f253..ca755a85ea 100644 --- a/docs/design-patterns/mapreduce-tree.mdx +++ b/docs/design-patterns/mapreduce-tree.mdx @@ -2,6 +2,9 @@ id: mapreduce-tree title: "MapReduce Tree" description: "Recursively splits a dataset into a binary tree of Child Workflows, processes leaves in parallel, then aggregates results back up the tree." +tags: + - Design Patterns + - Child Workflows --- import Tabs from '@theme/Tabs'; @@ -425,6 +428,7 @@ public class NodeWorkflowImpl implements NodeWorkflow { ## Common pitfalls +- **Too many direct children per Node.** Temporal's [guidance](/child-workflows#when-to-use-child-workflows) is that a single parent should not spawn more than 1,000 Child Workflow Executions. This applies to every Node in the tree, not just the Root — if a Node's own branching factor produces more than that many direct child Workflows, add another tree level rather than widening a single Node. - **Thundering herd.** The MapReduce Tree fans out exponentially. For large record sets, all leaf Activities start nearly simultaneously. Ensure your downstream system can absorb the burst, or switch to [Sliding Window](/design-patterns/sliding-window) for rate limiting. - **Signal storms.** If thousands of leaves all signal a single Node at the same time, the Node's signal queue can become a bottleneck. A two-level tree (Root → Nodes → Leaves) distributes this load; a deeper tree helps even more. - **History bloat in the Root Workflow.** Each child start and signal received adds events to the Root's history. For very large record sets, consider adding an extra tree level to keep the Root from receiving too many direct signals. diff --git a/docs/design-patterns/non-retryable-errors.mdx b/docs/design-patterns/non-retryable-errors.mdx index f82ba831ae..9e5cebff41 100644 --- a/docs/design-patterns/non-retryable-errors.mdx +++ b/docs/design-patterns/non-retryable-errors.mdx @@ -2,6 +2,9 @@ id: non-retryable-errors title: "Non-Retryable Errors" description: "Mark error types that will never succeed — such as validation failures or missing records — so Temporal fails fast instead of retrying indefinitely." +tags: + - Design Patterns + - Errors --- import Tabs from '@theme/Tabs'; @@ -472,6 +475,7 @@ try { - **Using the error message instead of a type name.** `RetryPolicy.NonRetryableErrorTypes` matches on type names, not message strings. Without a type name, the policy cannot identify the error. - **Swallowing the `ActivityError` without logging.** Non-retryable errors fail fast and silently if you do not catch and log them. Always log the failure before re-raising or returning an error result. - **Confusing non-retryable errors with Workflow failures.** A non-retryable `ActivityError` fails the Activity and delivers the error to the Workflow. The Workflow itself does not fail unless it re-raises the error without catching it. +- **Losing the flag by wrapping the error.** The SDK inspects only the **outermost** error to decide how to represent the failure to the Temporal Service — the Server's retry decision looks only at that top-level failure info, not at the `cause` chain. If your Activity catches a non-retryable `ApplicationFailure` and re-throws it wrapped in a plain language error or exception (for example, Go's `fmt.Errorf("...: %w", err)`), the SDK converts that outer error into a new, retryable failure and the `non_retryable` flag is silently lost. To add context without losing it, wrap the error in another Application Failure that carries the same non-retryable flag — see [The outermost error type determines retryability](/encyclopedia/application-failures#outermost-error-type). ## Related diff --git a/docs/design-patterns/parallel-execution.mdx b/docs/design-patterns/parallel-execution.mdx index 28047da6a2..e9cab2e249 100644 --- a/docs/design-patterns/parallel-execution.mdx +++ b/docs/design-patterns/parallel-execution.mdx @@ -2,6 +2,9 @@ id: parallel-execution title: "Parallel Execution" description: "Executes multiple Activities concurrently for maximum throughput with error handling and controlled parallelism." +tags: + - Design Patterns + - Activities --- import Tabs from '@theme/Tabs'; diff --git a/docs/design-patterns/performance-latency-patterns.mdx b/docs/design-patterns/performance-latency-patterns.mdx index d418bd30df..068b4ec610 100644 --- a/docs/design-patterns/performance-latency-patterns.mdx +++ b/docs/design-patterns/performance-latency-patterns.mdx @@ -2,6 +2,8 @@ id: performance-latency-patterns title: "Performance & Latency Patterns" description: "Pattern selection guide for reducing Workflow latency, with a comparison of the round-trips each pattern removes and their combined effect." +tags: + - Design Patterns --- import PatternCards from '@site/src/components/PatternCards'; @@ -39,15 +41,11 @@ The numbers below are approximate benchmarks based on a three-Activity transacti | [Early Return](/design-patterns/early-return) | ~265 ms | ~850 ms | All | | [Local Activities](/design-patterns/local-activities) | ~275 ms | ~275 ms | All | | [Early Return + Local Activities](/design-patterns/early-return-local-activities) | ~160 ms | ~275 ms | All | -| [Eager Workflow Start](/design-patterns/eager-workflow-start) + Local Activities | ~265 ms | ~265 ms | Go, Java, Python | -| Early Return + Local Activities + Eager Start | ~160 ms | ~265 ms | Go, Java, Python | +| [Eager Workflow Start](/design-patterns/eager-workflow-start) + Local Activities | ~265 ms | ~265 ms | Go, Java, Python, TypeScript, .NET | +| Early Return + Local Activities + Eager Start | ~160 ms | ~265 ms | Go, Java, Python, TypeScript, .NET | **First Response** is the time until the client receives an actionable result. **Total Latency** is the time until the Workflow fully completes. -:::tip[TypeScript users] -Eager Workflow Start is not available in the TypeScript SDK, but the latency gap is small (~30–50 ms per Workflow start). [Local Activities](/design-patterns/local-activities) and [Early Return + Local Activities](/design-patterns/early-return-local-activities) are fully supported and achieve competitive results: ~275 ms total latency and ~160 ms first-response latency respectively. -::: - ## Patterns in this section