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). 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:
- 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.
- 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
StartWorkflowExecutionresponse. - The Worker processes the Workflow Task immediately upon receiving the response. No separate poll is required.
- 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.
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.
- Python
- Go
- Java
# 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())
// starter.go — starts the Worker in the same process, then executes the Workflow eagerly
func main() {
c, err := client.Dial(client.Options{})
if err != nil {
log.Fatalln("Unable to create Temporal client:", err)
}
defer c.Close()
// Start the Worker non-blocking — it must share this process and client.
w := worker.New(c, TaskQueue, worker.Options{})
w.RegisterWorkflow(TransactionWorkflow)
w.RegisterActivity(ValidateTransaction)
w.RegisterActivity(SettleTransaction)
if err := w.Start(); err != nil {
log.Fatalln("Unable to start worker:", err)
}
defer w.Stop()
run, err := c.ExecuteWorkflow(context.Background(), client.StartWorkflowOptions{
ID: "eager-workflow-start-demo",
TaskQueue: TaskQueue,
EnableEagerStart: true, // Dispatch first WorkflowTask inline
}, TransactionWorkflow, TransactionRequest{Amount: 100.00, Currency: "USD"})
if err != nil {
log.Fatalln("Failed to start workflow:", err)
}
var result Transaction
if err := run.Get(context.Background(), &result); err != nil {
log.Fatalln("Workflow failed:", err)
}
fmt.Printf("Transaction complete: ID=%s Status=%s\n", result.ID, result.Status)
}
// Starter.java — starts the Worker in the same process, then executes the Workflow eagerly
public class Starter {
public static void main(String[] args) {
WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
WorkflowClient client = WorkflowClient.newInstance(service);
// The Worker must share this process and client for eager dispatch.
WorkerFactory factory = WorkerFactory.newInstance(client);
io.temporal.worker.Worker worker = factory.newWorker(Shared.TASK_QUEUE);
worker.registerWorkflowImplementationTypes(TransactionWorkflow.Impl.class);
worker.registerActivitiesImplementations(new Activities.Impl());
factory.start();
TransactionWorkflow workflow = client.newWorkflowStub(
TransactionWorkflow.class,
WorkflowOptions.newBuilder()
.setTaskQueue(Shared.TASK_QUEUE)
.setWorkflowId("eager-workflow-start-demo")
.setDisableEagerExecution(false) // false = enable eager dispatch
.build()
);
Shared.Transaction result = workflow.processTransaction(
new Shared.TransactionRequest(100.00, "USD"));
System.out.printf("Transaction complete: ID=%s Status=%s%n",
result.id(), result.status());
factory.shutdown();
}
}
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 Start | Eager Workflow Start | |
|---|---|---|
| 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 |
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 deferw.Stop(). In Python, useasync with Worker(...). In Java, callfactory.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
WorkflowClientinstance (Java),client.Client(Go), orClient(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
ExecuteWorkflowis 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.
Related patterns
- Local Activities — eliminates per-Activity server round-trips; pairs naturally with Eager Workflow Start
- Early Return + Local Activities — minimum first-response latency via Update-with-Start plus Local Activities
- Early Return — returns early to the client via Update-with-Start