avatar
Larry HsiaoMobile App Developer · Android & Flutter

Why Did a Failed Offline Request Send Itself After Reconnecting?

2026-06-17 · 5 min read

Note: This article was completed by AI (Claude) from my initial notes and thoughts.

This puzzled me for years: a network request that should have failed would sometimes, after I plugged the network back in, send itself anyway.

I hit it again recently in a Flutter app, and this time I finally pieced together what was going on. The answer is deflating — it's not a clever "auto-sync on reconnect" feature. It's a race against the TCP connect timeout.

The symptom

The app is offline-first: when you save a record, it's written to the local encrypted database first, then a background HTTP upload is fired off fire-and-forget. Nothing is lost offline; the upload is best-effort.

(Everything below was tested on iOS — the connect-timeout and negative-caching behaviors vary by OS, so don't take the second counts as cross-platform constants.)

While testing, I saw this:

  1. Turn off wifi, save a record — the upload request doesn't fail immediately; it hangs.
  2. A while later, I turn wifi back on and do something completely unrelated.
  3. That record, the one that "should have failed," uploads on its own.

It looks exactly like the app detected the network coming back and re-sent. But I was certain — there is no connectivity-listening code anywhere in this app. So how did it get sent?

The misread: "it must have auto-sync on reconnect"

This is the most intuitive — and most wrong — explanation. With no connectivity listener, nobody triggers a re-send. The request wasn't re-sentit was only ever sent once; that one attempt just took a very long time to complete.

The phrase "a very long time" is the whole story.

The real cause: the request was waiting, not failing

Unpack what an HTTP POST does with no network:

And Dart's http.Client.post has no app-level timeout by default, so the Future stays pending for the entire connect-timeout window.

Add that the request is fire-and-forget (unawaited, or .then() with no await): the UI moved on long ago, but the request is still alive in the background:

// Roughly this — not the actual code
unawaited(client.post(uri, body: payload)); // no timeout, not awaited
// ↑ Offline, this Future stalls inside the OS connect-timeout window — up to ~30s

So the "magic" happens: if I bring the network back before the connect timeout expires, the next retransmitted SYN finally gets a reply → the connection establishes → the POST still queued in the background completes.

It looks like auto-sync. It's really just a race to reconnect before the connect timeout fires.

Worth stressing: this is deeply unreliable. Reconnect a step too late — the timeout already fired — and the request failed long ago; nothing uploads until something explicitly retries.

Corroborating clue: first one slow, the rest fail instantly

There's another clue that backs the mechanism: the first offline save hangs ~30s, but the second and third saves fail instantly.

This is classic negative caching: the first connect to an unreachable host waits the full OS connect timeout; once it fails, the OS caches the "host unreachable / DNS negative" result, so later attempts fail at once instead of waiting.

First slow, the rest fast — a network-stack behavior, not anything the app does.

A sibling lesson: "an interface is up" ≠ "the internet is reachable"

To detect "offline" we used connectivity_plus's checkConnectivity(). But it reads the operating system's network-interface state, not whether the internet is actually reachable.

This bit me: on an iPad with wifi off but connected to a Mac over USB for debugging, checkConnectivity() returned ConnectivityResult.other — the USB debug link counts as a "connected" interface — so the app thought it was online, ran the upload anyway, and hung. My first fix (checking any != none) was wrong for exactly this reason.

The right approach: count only genuinely internet-capable interfaces (wifi / mobile / ethernet / vpn) as online; treat other / bluetooth / none as offline.

The lesson: interface presence is not internet reachability. connectivity_plus answers "is any interface up?", not "can I reach the internet?". For the latter you need an actual probe (a DNS lookup or request with a short timeout) — trading the "instant" answer for a little latency.

Wrap-up: draw your own boundaries instead of betting on a socket that hasn't died yet

To make offline behavior deterministic rather than a gamble against the connect timeout, the direction is clear:

Three lines to close: draw the timeout boundaries yourself, don't trust a socket still gasping for air, and "an interface is up" never means "the internet is reachable."