Event Accumulator use cases
The Accumulator pattern is well suited when events related to the same entity or group arrive from multiple producers and can be processed together as batches, when downstream systems prefer batched calls rather than one call per event, and when at-least-once event delivery makes deduplication necessary at the collection layer.
Event Accumulator unsuitable use cases
The Accumulator pattern is not a good fit for use cases that require processing every event individually in strict order, for cases where the batch size is known in advance and all events arrive within a short deterministic window (a standard workflow is sufficient), or when events for different keys must be correlated at processing time (consider fan-in with for example Child Workflows or multiple levels of Accumulator).
Event Accumulator benefits
The Accumulator pattern reduces downstream load by consolidating many individual events into a single batch and activity call. Signal-With-Start eliminates client-side coordination logic for starting or locating the collector workflow. Temporal's durable execution guarantees that accumulated state survives Worker restarts, and Continue-As-New prevents history growth from becoming a long-term problem.
Event Accumulator trade-offs
Events are not processed until the inactivity timeout fires or a flush signal is sent, introducing intentional latency. The timeout value is a domain-specific trade-off between latency and batch size. If producers stop sending signals but never send a flush, the workflow holds open resources until the timeout fires.
Event Accumulator pitfall: non-deterministic workflow IDs
Non-deterministic or random workflow IDs cause multiple accumulator instances to be created for the same group, splitting the batch. Always derive the workflow ID deterministically from the group key.
Event Accumulator pitfall: separate start and signal calls
Calling start followed by a separate signal is not atomic. A signal sent between the two calls will be lost if the workflow has not yet started. Always use Signal-With-Start instead.
Event Accumulator pitfall: activities in signal handlers
Calling activities inside signal handlers is a pitfall. Signal handlers run synchronously in the workflow thread and must not block or call activities. Buffer the item and let the main loop handle activity calls.
Event Accumulator pitfall: workflow completion and new signals
Assuming workflow completion means all events were captured is a pitfall. Producers that send signals after the workflow completes will start a new accumulator instance. Decide whether this is intentional (a new accumulation window) or an error.
Event Accumulator pitfall: empty signal queue before Continue-As-New
Not draining the signal queue before calling Continue-As-New is a pitfall. Any signal that arrives between the Continue-As-New decision and the actual Continue-As-New execution can be lost if the signal buffer is not empty when Continue-As-New fires. All SDK implementations guard against this by re-checking the unprocessed queue before continuing; do not remove that guard or call Continue-As-New unconditionally on the history-size trigger.
MapReduce Tree pattern overview and use case
The MapReduce Tree pattern recursively splits a dataset into a binary tree of Child Workflows, processes leaves in parallel, and aggregates results back up the tree. Each node fans out to N sub-slices (two by default). Use this pattern when you need maximum throughput for an embarrassingly parallel workload and downstream systems can absorb an unbounded burst of concurrent requests. It is best suited for embarrassingly parallel workloads where speed matters more than rate limiting.
MapReduce Tree vs Batch Iterator and Sliding Window concurrency
The Batch Iterator and Sliding Window patterns bound concurrency, which limits throughput. MapReduce Tree avoids a fixed concurrency window, allowing unbounded parallel processing. Use MapReduce Tree when you need to process a large record set as fast as possible and downstream systems can handle the load, and when you need to handle record sets larger than what a single Workflow's concurrency limits allow without pre-partitioning data into fixed chunks before the job starts.
MapReduce Tree execution flow
A Node Workflow receives a slice of records. If the slice is small enough (at or below a configurable leafThreshold), it starts one Leaf Workflow per record. Otherwise it splits the slice into N sub-slices and starts N Node child Workflows recursively. Each Leaf Workflow runs the actual processing Activity and signals its result back to its parent Node. Each Node aggregates the results it receives and signals them up to its own parent. The Root Node returns the final aggregated result.
MapReduce Tree best practice: leafThreshold configuration
Set a leafThreshold to control tree depth. A threshold of 3–10 records per leaf is typical. Too small a threshold creates excessive Workflow overhead; too large prevents full parallelism.
MapReduce Tree best practice: MAX_DEPTH guard
Set a MAX_DEPTH guard to prevent recursive fan-out from producing extremely deep trees for large record sets. Fail fast if depth exceeds your expected maximum, for example log2(totalRecords / leafThreshold) + 2.
MapReduce Tree best practice: separate Node and Leaf responsibilities
Avoid external writes in Node Workflows. Node Workflows only aggregate results from children. Leaf Workflows perform the actual work. Keeping the roles separate prevents duplicate external writes if a Node is retried.
MapReduce Tree best practice: use signals for result aggregation
Use signals for result aggregation, not return values. A parent cannot directly await a child started in a previous Workflow run. Signals decouple the result delivery from the parent-child lifetime, making the pattern resilient to replays.
MapReduce Tree best practice: skip reduce phase if results not needed
If you only need the side effects of processing each record (writes to a database, messages sent), omit the signal-back entirely and set PARENT_CLOSE_POLICY_ABANDON on all children.
MapReduce Tree best practice: Leaf Workflows vs Activities for lighter workloads
Consider replacing Leaf Workflows with Activities for lighter workloads. Leaf Workflows give each record its own Event History, independent cancellation, and dedicated visibility in the UI — useful when per-record observability matters. If those properties are not required, executing the processLeaf Activity directly from a Node Workflow reduces overhead. A successful child workflow and a successful Activity each add three Events to the parent's history, but a Leaf Workflow also maintains a separate Event History of its own and emits an extra signal back to the Node, so child workflows produce more overall Events than Activities. The Temporal documentation recommends starting with Activities and adopting child workflows only when there is a clear need.
MapReduce Tree pitfall: 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 for rate limiting.
MapReduce Tree pitfall: 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.
MapReduce Tree pitfall: history bloat in 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.
MapReduce Tree pitfall: external writes from Node Workflows
Nodes may be retried. Any external write in a Node Workflow will be executed multiple times. Keep all side effects in Leaf Workflows (or Activities called by Leaves).
Pick First pattern overview and use cases
The Pick First pattern executes multiple Activities in parallel and returns the result of whichever completes first, then cancels the remaining Activities. It is suitable for racing multiple approaches to the same task, implementing timeout alternatives, optimizing for fastest response when multiple options are available, racing multiple data sources (primary vs backup), trying multiple algorithms and picking the fastest, implementing fallback strategies with timeout, optimizing for latency when multiple options exist, and testing multiple service endpoints for fastest response.
Pick First pattern not suitable for
The Pick First pattern is not a good fit when you need results from all Activities (use parallel execution), Activities have side effects that should not be cancelled, order matters (use sequential execution), or all Activities must complete.
Pick First Activity cancellation support via heartbeats
For the Pick First pattern to work efficiently, Activities must detect cancellation via heartbeats. Activities should call `activity.heartbeat()` (or equivalent in their SDK) on each iteration. When cancellation is detected through the heartbeat mechanism, the Activity performs cleanup and exits. In Go, the Activity checks `ctx.Done()` in a select statement. In TypeScript, the Activity calls `Context.current().cancellationSignal.throwIfAborted()`. In Java, `Activity.getExecutionContext().heartbeat()` throws `CanceledFailure` if cancellation was requested. In Python, cancellation is detected via `asyncio.CancelledError`.
Pick First pattern common pitfall: missing heartbeats
Activities must heartbeat to detect cancellation. Without heartbeats, cancelled Activities continue running until their StartToCloseTimeout expires, wasting resources.
Pick First pattern common pitfall: not waiting for cancellation cleanup
Without configuring the cancellation type to wait for completion (for example, `WaitForCancellation: true` in Go, `WAIT_CANCELLATION_COMPLETED` in other SDKs), fetching a cancelled Activity's result returns a cancellation error immediately, before the Activity has finished cleanup. Configure this setting if you need to wait for cleanup to complete.
Pick First pattern common pitfall: ignoring errors from winning Activity
The first Activity to complete might return an error. Always check the result for errors rather than assuming success.
Pick First pattern common pitfall: forgetting to cancel remaining Activities
If you forget to cancel the shared context or scope after receiving the first result, the remaining Activities continue running indefinitely.
Pick First pattern best practices
Use heartbeats: Activities must heartbeat to detect cancellation quickly. Configure cancellation wait behavior: Decide if the Workflow should wait for cleanup to complete before returning. Handle cancellation in Activities: Activities must check for cancellation signals and exit cleanly. Use a shared cancellable context: Use a single cancellable context or scope for all raced Activities. Track futures or tasks: Keep references to all futures or tasks if waiting for cleanup. Set Activity timeouts: Configure appropriate StartToCloseTimeout and HeartbeatTimeout. Log cancellations: Log when Activities are cancelled for observability. Design idempotent Activities: Ensure Activities handle cancellation safely.
Pick First pattern benefits and trade-offs
Benefits: The pattern returns as soon as the fastest option completes, optimizing for latency. Unnecessary work is cancelled automatically. Each SDK's race mechanism ensures replay consistency, and cancellation cleanup is handled properly. Trade-offs: Cancelled Activities may have done partial work. Activities need heartbeats to detect cancellation quickly. Activities do not cancel instantly (they wait for the next heartbeat). You must implement proper cancellation handling in Activities. Only the first result is used; others are discarded.
Message handler execution order in Workflow loop
Temporal runs a loop that processes messages in the order they were received, followed by making progress in the Workflow's main method. This execution happens on a single thread, so while parallelism is not a concern, concurrency issues can arise if Signal and Update handlers can block, potentially running interleaved with the main Workflow and with one another.
Messages processed before first Workflow run
Message handlers run before the first execution of the Workflow's main method in several scenarios: when using Signal-with-Start, when the Worker experiences delays such as Task Queue backlog, and when messages arrive immediately after a Workflow continues as new but before it resumes. This means handlers may try to read uninitialized instance variables.
Workflow initialization best practice
Initialize the Workflow's state before handling messages to prevent handlers from reading uninitialized instance variables. For all languages except Go and TypeScript, use the constructor annotated as a Workflow Initializer and take the same arguments as the Workflow's main method. In Go and TypeScript, register any message handlers only after completing initialization. Note that blocking calls cannot be made from the constructor; if blocking is needed, make Signal or Update handlers wait for an initialization flag.
Update Validator definition and behavior
When defining an Update handler, you may optionally define an Update Validator, a read operation responsible for accepting or rejecting the Update. If the Validator accepts, the Update becomes part of the Workflow's history, the client is notified it has been Accepted, and the Update handler runs until it returns a value. If the Validator rejects, the client is informed it was Rejected and the Workflow has no indication it was ever requested, similar to a Query handler. Validators are not allowed to block like Queries.
Update handler exceptions fail the Update only
Throwing an exception from an Update handler, rejecting the Update from a Validator, or allowing a failing Activity or Child Workflow to exhaust retries will fail the Update and cause the client to receive the error. Unlike with Signals, the Workflow will keep going in these cases. If any other exception is thrown, it causes a Workflow Task Failure, which gets stuck and retries the handler until fixed, but this will cause a delay for clients waiting for the Update result.
Synchronous message handlers are atomic
Synchronous handlers are those that don't kick off any long-running operations or otherwise block. They are guaranteed to run atomically.
Request ID deduplication for Signals and Updates
In addition to application-level identifiers, both Signals and Updates automatically use request IDs to deduplicate retried client calls. No action is needed to enable this.
Signal idempotency using custom idempotency key
For Signals to run exactly once and be idempotent in cases where the same Signal is delivered twice, use a custom idempotency key sent as part of the signal inputs and implement the deduplication in the Workflow code.
Update ID deduplication by server
For Updates, Temporal handles deduplication on the server according to the Update ID. The Update ID is set automatically to a UUID, but can be set manually. However, if using Updates with Continue-As-New, deduplication must be implemented in Workflow code since server-side Update ID deduplication is per Workflow run only.
Update completion after handler returns value
Once the Update handler is finished and has returned a value, the Update operation is considered Completed.
Signal handler exceptions cause Workflow failure
In Signal handlers (all SDKs except Go), throw Application Failures only for unrecoverable errors, because the entire Workflow will fail. Similarly, allowing a failing Activity or Child Workflow to exhaust its retries will cause the entire Workflow to fail. If any other exception is thrown, by default it will cause a Workflow Task Failure, which means the Workflow gets stuck and will retry the handler periodically until the exception is fixed.
Finishing handlers before Workflow completion
Signal and Update handlers should generally be finished running before the Workflow run completes or continues as new. For some Workflows, this means explicitly checking that all handlers have completed before finishing by awaiting a condition called All Handlers Finished at the end of the Workflow. If handlers do not need to complete, you can specify the handler's Handler Unfinished Policy as Abandon to turn off warnings, but note that clients waiting for Updates will get Not Found errors if they're waiting for Updates that never complete before the Workflow run completes.
Injecting work into main Workflow pattern
An advanced pattern where message handlers put work into a queue that is then picked up and processed in an event loop written in the main Workflow. This allows accumulating several messages before acting on any of them and avoids using concurrency primitives like mutexes and semaphores by serializing message handling inside the main Workflow.
Asynchronous tasks in message handlers yield control
When a message handler needs to wait for long-running operations such as executing an Activity, the handler will yield control back to the Workflow loop. This means handlers can have race conditions if not careful. Handlers should be guarded with concurrency primitives like mutexes or semaphores provided for Workflows in most languages.
Signal and Update handlers can block using Wait Condition
A Signal or Update handler can block waiting for the Workflow to reach a certain state using a Wait Condition. This allows handlers to wait for the Workflow to be ready to process them before proceeding.