elsa-workflows/elsa-core · error · TimeoutException
Signal ' ' timed out after milliseconds.
Error message
Signal '{signal}' timed out after {millisecondsTimeout} milliseconds. What it means
SignalManager.WaitAsync waits on a TaskCompletionSource for the signal while a Task.Delay races against it; when the delay wins, the pending signal entry is removed and a TimeoutException is thrown. This means the awaited signal was never triggered (or was triggered before the wait started / with a different name) within the configured timeout (default 60000 ms).
Solutions
- Verify the workflow actually calls TriggerSignal with the exact same signal name (check spelling and casing).
- Ensure WaitAsync is called before or concurrently with the workflow run so the TCS is registered before the signal fires.
- Increase the millisecondsTimeout argument (or default 60000) when workflows are slow, especially in CI.
- Check workflow execution logs for faults that prevent the signal activity from running; ensure a fresh SignalManager per test to avoid cross-test signal leakage.
Example fix
// before
var result = await signals.WaitAsync<string>("approved"); // times out at 60s
// after
await workflowClient.RunWorkflowAsync(...); // ensure run actually reaches TriggerSignal("approved")
var result = await signals.WaitAsync<string>("approved", 120000); Defensive patterns
Strategy: try-catch
Validate before calling
// confirm the workflow will signal before waiting
Assert.True(workflowDefinition.Activities.Any(a => a.Type == "Signal" && a.GetSignalName() == "approved"),
"Workflow does not contain an activity that triggers signal 'approved'."); Try / catch
try
{
var result = await signals.WaitAsync<string>("approved", 120000);
}
catch (TimeoutException ex)
{
Assert.Fail($"Signal never arrived: {ex.Message}. Check workflow faulted state and signal name.");
} Prevention
- Call WaitAsync before starting the workflow so the TCS is registered first.
- Use generous, environment-aware timeouts (CI is slower than local).
- Use one SignalManager per test to prevent cross-test signal theft.
- Assert signal names with constants shared between workflow and test.
When it happens
Trigger: Calling signalManager.WaitAsync("signal-name") when no code calls TriggerSignal("signal-name") within the timeout; the signal was triggered before WaitAsync registered its TCS; workflow failed or never reached the signaling activity; timeout parameter set too low for a slow workflow.
Common situations: The workflow under test throws or blocks before reaching the signal; a typo or case mismatch between signal names in trigger and wait; parallel tests sharing one SignalManager steal each other's signals; CI machines are slower than the hard-coded timeout.
Understand the failure class
Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Signal ' ' was not of type ' '.
- Workflow definition with ID
- Build() must be called before accessing services
- Timeout
- Could not acquire distributed lock with key
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/7a719f8f714df349.
Report an issue: GitHub.
Appendix: source
Thrown at src/common/Elsa.Testing.Shared.Component/Services/SignalManager.cs:29
var result = await WaitAsync(signal, millisecondsTimeout);
if(result is not T typedResult)
throw new InvalidCastException($"Signal '{signal}' was not of type '{typeof(T).Name}'.");
return typedResult;
}
public async Task<object?> WaitAsync(object signal, int millisecondsTimeout = 60000)
{
var taskCompletionSource = GetOrCreate(signal);
using var cancellationTokenSource = new CancellationTokenSource(millisecondsTimeout);
var delayTask = Task.Delay(millisecondsTimeout, cancellationTokenSource.Token);
var completedTask = await Task.WhenAny(taskCompletionSource.Task, delayTask);
if (completedTask == delayTask)
{
_signals.TryRemove(signal, out _);
throw new TimeoutException($"Signal '{signal}' timed out after {millisecondsTimeout} milliseconds.");
}
cancellationTokenSource.Cancel();
_signals.TryRemove(signal, out _);
return await taskCompletionSource.Task;
}
public void Trigger(object signal, object? result = null)
{
var taskCompletionSource = GetOrCreate(signal);
if (taskCompletionSource.Task.IsCompleted)
return;
taskCompletionSource.SetResult(result);
}
private TaskCompletionSource<object?> GetOrCreate(object eventName)View on GitHub (pinned to fe9217bdfa)