Skip to main content

Eager Workflow Start

View Markdown
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.

Overview

When you call ExecuteWorkflow, the Temporal server normally stores the new Workflow execution, then routes the first Workflow Task through its Matching Service to an available Worker. Eager Workflow Start short-circuits this routing: the server returns the first Workflow Task inline in the StartWorkflowExecution response, and the co-located Worker processes it immediately—without a separate polling round-trip.

Numbered walkthrough:

  1. In a normal start, the server queues the Workflow execution and the Matching Service waits for an available Worker slot. The Worker polls, picks up the task, runs it, and reports back—adding an extra server round-trip.
  2. With Eager Workflow Start enabled, the server detects that the requesting client has a co-located Worker with an available slot. Instead of queuing the task, the server attaches the first Workflow Task to the StartWorkflowExecution response.
  3. The Worker processes the Workflow Task immediately upon receiving the response. No separate poll is required.
  4. If the server cannot fulfill the eager request (e.g., no local slot is available), it falls back silently to normal dispatch. Your code does not need to handle this case explicitly.

Problem

Even with Local Activities eliminating per-Activity server round-trips, the Workflow's first Workflow Task still requires a scheduling round-trip through the Temporal Matching Service. This adds latency that is unavoidable in a distributed deployment where Workers are separate from the caller.

For applications where the starter and Worker share the same deployment unit—such as a request-handling service that also runs Workers—this Matching overhead can be eliminated.

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.

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.

# starter.py — starts the Worker in the same process, then executes the Workflow eagerly
import asyncio
from temporalio.client import Client
from temporalio.worker import Worker
from workflows import TransactionWorkflow
from activities import validate_transaction, settle_transaction
from shared import TASK_QUEUE, TransactionRequest

async def main():
client = await Client.connect("localhost:7233")

# The Worker must share this process and client for eager dispatch to work.
async with Worker(
client,
task_queue=TASK_QUEUE,
workflows=[TransactionWorkflow],
activities=[validate_transaction, settle_transaction],
):
result = await client.execute_workflow(
TransactionWorkflow.run,
TransactionRequest(amount=100.00, currency="USD"),
id="eager-workflow-start-demo",
task_queue=TASK_QUEUE,
request_eager_start=True, # Dispatch first WorkflowTask inline
)
print(f"Transaction complete: ID={result.id} Status={result.status}")

if __name__ == "__main__":
asyncio.run(main())
TypeScript SDK

The TypeScript SDK does not currently support Eager Workflow Start. Use Local Activities or Early Return + Local Activities for latency-sensitive TypeScript workflows.

When to use

Good fit:

  • The workflow starter and Worker run in the same deployment unit (e.g., 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

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 or Early Return + Local Activities for that use case

Benefits and trade-offs

Normal StartEager Workflow Start
Matching Service round-tripYes (~30–50 ms)No (eliminated)
Worker co-location requiredNoYes (same process + client)
Fallback behaviorN/AGraceful fallback to normal dispatch
TypeScript SDK supportYesNo
Configuration requiredNoneEnableEagerStart/request_eager_start/setDisableEagerExecution(false)
Self-hosted server flagN/AMay need system.enableEagerWorkflowStart=true

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 (e.g., 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.
  • 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.