The server has been back for a minute, and the client still says "reconnecting" — HubConnection.State is Disconnected, the Closed event has fired, and nothing is going to try again. WithAutomaticReconnect() did exactly what it documents: four attempts, then it stopped.

The parameterless WithAutomaticReconnect() retries after 0, 2, 10 and 30 seconds. If the fourth attempt fails — about 42 seconds after the drop — the connection closes for good and only your code can start it again. A server restart that takes longer than that, which a deploy or a database migration easily does, leaves every client stranded. Pass an IRetryPolicy whose NextRetryDelay never returns null, and the client keeps trying with backoff until the server answers.

The error

No exception — a timeline, abridged from the build log and the screenshots that proved it:

        server killed for ~15 s, then restarted
12:22   a full minute later: phone still shows "reconnecting…"
        HubConnection has Closed; nothing retries

after the fix:
12:27   server killed, restarted
12:28   pill gone by itself; Reconnected fired, group re-joined

Why it happens

The default policy is a sensible one for the place SignalR grew up: a browser tab. Four tries over about forty seconds cover a blip, and if the server is really gone the human will refresh the page. The client library can't know that your client is a phone in a coat pocket whose owner will never refresh anything.

Mechanically, the array overload behaves the same way: WithAutomaticReconnect(new[] { … }) walks your delays and, when it runs out, returns null — and null means stop. Only a policy object can answer "keep going" forever. One more edge: automatic reconnect only arms after a successful StartAsync. If the very first start fails, no policy runs, so a resume or a screen has to try again.

The fix

From QueueLive.cs, the app's one hub connection:

// NOT the default policy. WithAutomaticReconnect() tries at 0, 2, 10 and 30
// seconds and then gives up forever — fine for a browser tab someone will
// refresh, wrong for a phone in a car park. Keep trying, with backoff.
_hub = new HubConnectionBuilder()
    .WithUrl(endpoint.QueueHub)
    .WithAutomaticReconnect(new KeepTrying())
    .Build();

_hub.Reconnected += async _ =>
{
    await _hub.InvokeAsync("JoinBoard");   // groups don't survive a reconnect
    ConnectionChanged?.Invoke(true);
    Changed?.Invoke();                     // catch up on whatever we missed
};

/// 2s, 4s, 8s, 16s, then every 30s — and never returns null, so it never stops.
private sealed class KeepTrying : IRetryPolicy
{
    public TimeSpan? NextRetryDelay(RetryContext retryContext) =>
        TimeSpan.FromSeconds(Math.Min(30, Math.Pow(2, retryContext.PreviousRetryCount + 1)));
}

Two things in that block matter as much as the policy. The Reconnected handler re-joins the hub group, because group membership dies with the old connection; and it re-reads state, because every event during the gap was missed. The alternative to a policy is handling Closed yourself and calling StartAsync in a loop — the pattern the docs show — which works, but the policy keeps the reconnecting/reconnected events flowing to your UI.

What the AI got wrong: the default policy is the documented one, and it reached for it exactly as it would for a web page — because a web page is what it has seen most. Only a screenshot taken a minute too late showed that "automatic reconnect" has an expiry date.

Where it bit us

Season three, Part 4: live queue in your pocket — the catch of the part, with the "gave up" screenshot kept as evidence. The policy is in tag pocket-04 of the repo. The waiting-room TV from season one's Part 7 uses the same hub; it just has a human nearby. The lesson: a default is a guess about your client, and a phone is not a tab.

Frequently asked

How many times does SignalR WithAutomaticReconnect retry by default?
Four. The parameterless WithAutomaticReconnect retries after 0, 2, 10 and 30 seconds, and if the fourth attempt fails the connection moves to the Disconnected state, the Closed event fires, and the client never tries again on its own.
How do I make a SignalR client reconnect forever?
Pass an IRetryPolicy to WithAutomaticReconnect whose NextRetryDelay never returns null. Returning a delay such as exponential backoff capped at 30 seconds keeps the client retrying indefinitely; returning null is what tells SignalR to stop. Re-join hub groups in the Reconnected event because group membership does not survive a reconnect.
Why doesn't automatic reconnect work when the first StartAsync fails?
Because automatic reconnect only arms after a connection has started successfully. If the initial StartAsync throws, no retry policy runs; your code has to catch the exception and call StartAsync again later, for example when the app resumes or a screen that needs live data opens.

More decoded errors in the Fixes category; the season starts at Part 1.