microsoft/aspire · error · TimeoutException

Dashboard did not become ready within the expected time.

Error message

Dashboard did not become ready within the expected time.

What it means

During dashboard readiness polling, WaitForDashboardAsync cancels its wait when the dashboard startup budget elapses. If the cancellation fired due to the startup timeout (not user cancellation), the poll catch block rethrows as TimeoutException with this message. It means the dashboard process stayed alive but its health endpoint never returned successfully in time.

Solutions

  1. Retry the profiling capture on a less loaded machine or after freeing CPU/memory.
  2. Verify the dashboard URL configuration matches what aspire-managed was launched with.
  3. Check local firewall/proxy rules that could block loopback HTTP requests.
  4. Capture the dashboard's stdout/stderr via logs to confirm it is actually listening.
Defensive patterns

Strategy: retry

Try / catch

for (var attempt = 0; attempt < 2; attempt++)
{
    try { return await captureService.StartAsync(options, ct); }
    catch (TimeoutException) when (attempt == 0) { /* one retry on slow machine */ }
}

Prevention

When it happens

Trigger: Calling ProfileCaptureService.StartAsync where the dashboard process keeps running but the HTTP health poll never succeeds before dashboardStartTimeout expires — e.g. the poll's HttpClient timeout cancels while the overall startup budget is exhausted.

Common situations: Dashboard bound to a different URL than the one being polled (URL config mismatch); Kestrel extremely slow to bind under heavy machine load; firewall/proxy software interfering with loopback HTTP; a very short startup budget on a cold, slow machine.

Related errors


AI-assisted analysis of microsoft/aspire@25830f84bd (2026-09-16). Data as JSON: /api/errors/3e8c704b511ff591. Report an issue: GitHub.

Appendix: source

Thrown at src/Aspire.Cli/Profiling/ProfileCaptureService.cs:299

                    throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, DashboardCommandStrings.DashboardExitedWithError, exitCode));
                }

                try
                {
                    using var client = _httpClientFactory.CreateClient();
                    // ProfileCaptureOptions allocates a loopback ephemeral port before the private
                    // dashboard starts. StartAsync passes that exact URL to Kestrel via
                    // --urls/ASPNETCORE_URLS, so probing _options.DashboardUrl verifies the collector
                    // bound to the random port reserved for this capture session.
                    using var response = await client.GetAsync(_options.DashboardUrl, linkedCts.Token).ConfigureAwait(false);
                    if (response.IsSuccessStatusCode)
                    {
                        return;
                    }
                }
                catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
                {
                    throw new TimeoutException(DashboardCommandStrings.DashboardStartTimedOut);
                }
                catch (HttpRequestException)
                {
                    // Connection refused while Kestrel is still binding is expected during startup.
                    // Keep polling until either the dashboard responds, exits, or the startup budget
                    // expires.
                }

                try
                {
                    await Task.Delay(pollInterval, _timeProvider, linkedCts.Token).ConfigureAwait(false);
                }
                catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
                {
                    throw new TimeoutException(DashboardCommandStrings.DashboardStartTimedOut);
                }
            }
        }

View on GitHub (pinned to 25830f84bd)