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). Supported by the Go, Java, Python, TypeScript, and .NET SDKs.

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 (for example, 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), 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.

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 for the canonical server-side reference.

# 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())
.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

Good fit:

  • 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
  • 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)
  • 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
SDK supportAllGo, Java, Python, TypeScript, .NET
Configuration requiredNoneEnableEagerStart/request_eager_start/setDisableEagerExecution(false)/requestEagerStart/RequestEagerStart
Self-hosted server flagN/AOn 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), 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.
  • 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.

Patterns

References