Skip to content
article

Flutter apps for firewall season: making your app usable when Pakistani internet is crawling

Timeouts, polite retries, cache first, an outbox for writes, and the splash screen that kills your app

Hamza Butt@hamza_flutter

August 6, 2026 · 3 min read

Yaar, if you build apps in Pakistan you know the seasons. Summer, monsoon, wedding season, and firewall season. When the internet slows down "for reasons", VPNs crawl, WhatsApp images do not load, and my clients in Dubai think our app is broken. Our app is not broken. The pipe is.

So this is what we changed in our Flutter apps. We are a two person studio, the clients are in Dubai and Riyadh, but most of our test users are in Lahore. Chalo.

1. connectivity_plus tells you the network type, not whether the internet works

It says "wifi" while the wifi is connected to a router that is connected to nothing. Do not show "You are online ✅" based on it. If you really need to know, ping your own health endpoint with a short timeout.

2. Set real timeouts

final dio = Dio(BaseOptions(
  baseUrl: 'https://api.example.com',
  connectTimeout: const Duration(seconds: 8),
  receiveTimeout: const Duration(seconds: 25),
  sendTimeout: const Duration(seconds: 25),
));

Short connect timeout, because a TLS handshake that hangs is usually dead. Longer receive timeout, because a slow response is still a response.

3. Retry, but like a gentleman

Future<T> withRetry<T>(Future<T> Function() fn, {int maxAttempts = 4}) async {
  final rng = Random();
  var attempt = 0;
  while (true) {
    try {
      return await fn();
    } on DioException catch (e) {
      attempt++;
      final retryable = e.type == DioExceptionType.connectionTimeout ||
          e.type == DioExceptionType.receiveTimeout ||
          e.type == DioExceptionType.connectionError;
      if (!retryable || attempt >= maxAttempts) rethrow;
      final base = 500 * (1 << attempt); // 1s, 2s, 4s
      await Future.delayed(Duration(milliseconds: base + rng.nextInt(base ~/ 2)));
    }
  }
}

Only retry GETs and requests that carry an idempotency key. Retrying a payment POST without a key is how you charge someone twice, ask any fintech dev.

4. Cache first, network second

Show the last data you had immediately, with "updated 12 min ago", then refresh in the background. We use drift (SQLite) for lists and a small key-value store for settings. Users forgive old data. They do not forgive a spinner.

5. An outbox for writes

When the user submits a form with no network, put it in a local outbox table with a UUID, show it as "sending", and sync when the network comes back. The UUID goes out as the idempotency key, so syncing twice does no harm.

6. Images will kill you

CachedNetworkImage(
  imageUrl: product.thumbUrl,
  memCacheWidth: 300,
  placeholder: (_, __) => const ColoredBox(color: Color(0xFFEDEDED)),
  errorWidget: (_, __, ___) => const Icon(Icons.image_not_supported_outlined),
)

Ask the backend for thumbnails. A 4MB photo from the client's iPhone in a product list is a crime. memCacheWidth decodes it smaller so cheap phones do not run out of memory.

7. Do not block startup on the network

If your splash screen waits for remote config, in firewall season your app never opens. Ship defaults, start, update the config in the background.

8. Test with a bad network on purpose

The Android emulator has network speed and latency settings under Extended controls. Set it to EDGE and use your own app for ten minutes. You will fix five things the same day. Guaranteed.

Koi gal nahi, the internet always comes back. Your users will remember which apps still worked when it did not.

Written by

Hamza Butt

Flutter dev, Lahore. Apps for clients in Dubai and Riyadh. Internet slow ho to samajh jao firewall ka mausam hai. Chai, cricket, Lahori nashta.

Get the best of #thumb_zone weekly

One email each Monday with the most useful posts. No spam; unsubscribe in one click.

Comments

11

Sign in to join the conversation.

Sign in to comment
Tunde Adebayo@tunde_ade#2 in Thumb Zone
omo this is the bible for nigerian networks too. point 7 especially, i have seen apps that refuse to open because remote config is shy. abeg everyone read this
1 net votes
Hamza Butt@hamza_flutter#3 in Thumb Zone
Tunde bhai remote config on splash is the number one killer. Client wanted it for a "feature flag". The feature flag killed the feature 😂
1 net votes
Thiago Valadares@thiago_pj#4 in Thumb Zone
point 1 is so true. connectivity_plus said wifi while my router was dead and the client said the app was lying. it was lying, technically
1 net votes
Vivek Chauhan@vivek_backend1 rep in Thumb Zone
Very solid, Hamza bhai. "Only retry GETs and requests with an idempotency key" is the most important line here. I am contractually obliged to disagree with you about cricket, but on this post I cannot.
2 net votes
Hamza Butt@hamza_flutter#3 in Thumb Zone
Vivek yaar, agreement on the internet, what is happening 😂 thank you
1 net votes
Ayesha Siddiqui@laravel_ayesha1 rep in Thumb Zone
Firewall season 😭 sending this to our mobile guy right now. Point 6, our client uploads 6MB photos for a product list and then asks why the app is slow. Uff
0 net votes
Achieng Owino@achieng_devrel1 rep in Thumb Zone
This pairs nicely with my low-bandwidth API thread. Backend and app meet in the middle: small payloads and idempotency keys from the API, caching and polite retries in the app.
5 net votes
Giulia Bellandi@giulia_designs#8 in Thumb Zone
Point 4 from a designer: "updated 12 min ago" is UX gold! Users trust honest old data more than a spinner. Allora, stealing this for our fintech app ✨
0 net votes
Trey Robinson@techwithtrey#8 in Thumb Zone
point 8 is real talk. set the emulator to EDGE once and you'll never ship a 5MB hero image again
1 net votes
Anoop Menon@anoop_flutter#4 in Thumb Zone
Late but pwoli post machane! Kerala monsoon is our firewall season 😅 One more: pass a CancelToken to the retry, so if the user leaves the screen you stop retrying in the background
3 net votes
Hamza Butt@hamza_flutter#3 in Thumb Zone
Good point yaar, cancel on dispose. Adding it to my list
2 net votes