DEV Community

Roronoa
Roronoa

Posted on

Keep Cloud Fallback Out of Radio Wake After Airplane Mode

You sit with a Pixel on Android 14 or an iPhone on iOS 18, sample chat already installed. The screen is foregrounded, Wi-Fi is associated, and Battery Saver is still off for this first pass. You submit a short prompt, then enable airplane mode before any token reaches the bubble. That single transition tells you whether onboarding shipped an offline contract or only a cafeteria Wi-Fi demo.

Hybrid chat usually fails this first-hour check

Hybrid samples keep an on-device engine for the common prompt and a cloud client for everything else. The cloud client usually lives behind a retry interceptor, a flavor URL, or a latency flag that panics too early. Airplane mode should make that URL unusable, but interceptors still open DNS and can wake cellular when the mode lifts. Your first PR should freeze that client whenever the OS path reports no validated network.

This is not a model-quality argument and not a placement essay about phones versus regional GPUs. It is a junior onboarding contract: the first hour, the first pull request, and the first rollback of the fallback host. If the typing indicator hangs, then suddenly talks to the network after radios return, the demo you saw on Wi-Fi was not engineering yet.

Record these facts before you touch product code

Write the environment into the PR description so a reviewer can replay the same transition.

  • Device marketing name, SoC if you know it, and the exact OS build string
  • App stack and versions: native, React Native, or Flutter, plus the HTTP client library
  • Starting network: office Wi-Fi, cellular only, or already offline
  • Permission state for microphone, local network, and background refresh, even if unused
  • Power state: charging, Battery Saver / Low Power Mode, or neither
  • The lifecycle step: airplane mode during an in-flight prompt, not after the bubble finishes

Do not treat an emulator airplane toggle as the same experiment as a physical radio. Emulators often fake connectivity without the DNS, interface, and wakeup behavior you will see on hardware.

Inventory the checkout in the first hour

You should grep the repository before rewriting prompts, because fallback hostnames hide in more places than .env.example. Run this from the app root and paste the hits you actually intend to gate.

# Record matching paths in the PR. Do not commit secrets you uncover.
rg -n -i "https?://|inference|fallback|baseUrl|BASE_URL|generativelanguage" \
  --glob '!**/node_modules/**' --glob '!**/Pods/**' --glob '!**/.git/**'

rg -n -i "OkHttp|NSURLSession|Alamofire|Dio|chopper|ConnectivityManager|NWPathMonitor" \
  --glob '!**/node_modules/**'
Enter fullscreen mode Exit fullscreen mode

Then write a three-line inventory a teammate can challenge.

  1. On-device engine and the packaged asset or module that actually loads it.
  2. Cloud fallback class and the hostname it resolves in this flavor.
  3. Build flag that must prevent constructing that client when the path is unvalidated.

If you cannot name those three things after the first hour, you are not ready to claim offline support in the pull request.

Proposed airplane-mode experiment

This is a proposed single-device experiment, not a lab result with invented first-token times. Copy the steps, fill in your versions, and report whether the UI recovered, restarted, or silently disappeared.

Preconditions you should not skip

You need a physical device, a flavor that still contains the on-device model, and a build that still compiles the cloud client. Keep Battery Saver off for pass A so power policy is not the hidden variable. Skip VPN profiles for this hour, because leftover tunnels make airplane mode look satisfied.

Steps

  1. Launch the chat screen and confirm the composer is focused in the foreground session.
  2. Confirm Battery Saver or Low Power Mode is off so pass A has a clean power baseline.
  3. Send a prompt the on-device path should handle without tools, browsing, or extra host calls.
  4. Enable airplane mode within one second so Wi-Fi and cellular drop during the in-flight request.
  5. Wait thirty seconds without tapping timeouts, unlocking dialogs, or backgrounding the app.
  6. Note a local answer, a closed offline string, or a spinner that never resolves into UI.
  7. Repeat pass B with Battery Saver or Low Power Mode already enabled before you submit.
  8. Repeat pass C by enabling airplane mode first, then sending a cold prompt with radios down.

Contract you can assert without fake benchmarks

  • Pass A answers on-device or fails closed with offline copy, never with a hostname error string.
  • Pass A leaves no DNS lookup and no TLS handshake in logcat or Console after the airplane toggle.
  • Pass B may stretch token time under Low Power Mode, but it must not flip the flag toward cloud.
  • Pass C must not enqueue a retry that fires later when the user disables airplane mode.
  • Restoring radios without a resubmit must not replay the prompt against the fallback host.

If a retry fires when radios return, the first PR is incomplete even when the Wi-Fi demo looked polished.

# Android: capture after pass A, before you disable airplane mode.
adb logcat -d | rg -i "okhttp|dns|tls handshake|fallback|Unable to resolve host"
Enter fullscreen mode Exit fullscreen mode

On iOS, filter Console for your bundle identifier during the same window and look for nw_connection or NSURLSession work that starts after the toggle. Any new connection there is a failed gate, not a flaky staging anecdote.

Artifact: refuse cloud work before DNS starts

Keep the gate beside the HTTP client, not inside a prompt formatter that still constructs the call. The operating system already knows whether a validated path exists. Ask it before you allocate OkHttp, URLSession, or Dio.

Android gate (proposed Kotlin)

class InferencePathGate(
    private val connectivity: ConnectivityManager
) {
    fun allowCloudFallback(): Boolean {
        val network = connectivity.activeNetwork ?: return false
        val caps = connectivity.getNetworkCapabilities(network) ?: return false
        val hasTransport =
            caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
            caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
            caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
        val hasInternet =
            caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
            caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
        return hasTransport && hasInternet
    }
}

class AirplaneSafeInterceptor(
    private val gate: InferencePathGate
) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        if (!gate.allowCloudFallback()) {
            throw IOException("cloud fallback blocked: no validated path")
        }
        return chain.proceed(chain.request())
    }
}
Enter fullscreen mode Exit fullscreen mode

Call allowCloudFallback() on the same path you would have opened the client. Throwing before chain.proceed is the point: you never give the interceptor a chance to resolve a name.

iOS gate (proposed Swift)

import Network

final class InferencePathGate {
    private let monitor = NWPathMonitor()
    private let queue = DispatchQueue(label: "inference.path.gate")
    private var path: NWPath?

    func start() {
        monitor.pathUpdateHandler = { [weak self] newPath in
            self?.path = newPath
        }
        monitor.start(queue: queue)
    }

    func allowCloudFallback() -> Bool {
        guard let path, path.status == .satisfied else { return false }
        return path.usesInterfaceType(.wifi) ||
            path.usesInterfaceType(.cellular) ||
            path.usesInterfaceType(.wiredEthernet)
    }
}
Enter fullscreen mode Exit fullscreen mode

Do not treat path.status == .satisfied as permission to talk to the public internet. Airplane mode with a leftover VPN or local interface can still look satisfied until you check the transport you actually intended.

Cross-platform juniors: native gate, thin bridge

If you joined a React Native or Flutter repo, still implement the gate in native code for this first PR. JavaScript reachability plugins often lag the radio by about a second, which is enough for a retry interceptor to start work you cannot see in the JS console.

// Proposed only. Do not treat this plugin as the final airplane-mode gate.
import 'package:connectivity_plus/connectivity_plus.dart';

Future<bool> allowCloudFallbackJsLayer() async {
  final results = await Connectivity().checkConnectivity();
  if (results.contains(ConnectivityResult.none)) return false;
  return results.contains(ConnectivityResult.wifi) ||
      results.contains(ConnectivityResult.mobile) ||
      results.contains(ConnectivityResult.ethernet);
}
Enter fullscreen mode Exit fullscreen mode

Expose one method, allowCloudFallback(): Boolean, and refuse to import or construct the cloud SDK when it is false. The first PR can be that bridge plus the three airplane-mode passes, without a prompt rewrite.

First PR, then the first rollback of the host

Your pull request should carry the inventory, the three passes, and a rollback a teammate can run after you leave Slack. Rollback is not “delete the on-device model.” Rollback is “stop constructing the fallback client, then destroy the hostname you used while proving the gate.”

Work the rehearsal in this order so you do not debug against shared staging.

  1. Flip the flavor flag so the fallback client is never constructed in that build.
  2. Re-run pass C and confirm you still answer locally or fail closed with offline copy.
  3. Restore the flag and point FALLBACK_BASE_URL at a throwaway host you control.
  4. Re-run airplane mode and confirm that host’s access log stays empty, then delete the host.
# Uncommitted local flavor. Do not paste real tokens into the PR.
FALLBACK_ENABLED=true
FALLBACK_BASE_URL=https://your-throwaway-host.example
Enter fullscreen mode Exit fullscreen mode

A junior engineer should not prove this gate against the team’s long-lived staging inference box. That hostname shows up in other people’s captures, and it trains you to ignore airplane-mode failures because “staging was flaky today.”

When you need a personal fallback URL for step 3, a short-lived server is enough. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option you can use as that throwaway host, then discard after the rollback rehearsal. Keep the URL out of the default flavor once the path gate is proven.

Decision table to paste under the PR test plan

Condition Cloud client On-device engine Radio / DNS User-visible result
Wi-Fi, charging, foreground allowed only if local engine declines first choice normal answer or explicit local fail
Airplane during in-flight prompt must not start continue or cancel locally no new DNS answer, offline copy, or cancelled
Airplane before a cold prompt must not start run or fail closed no new DNS no spinner that later hits cloud
Low Power Mode plus airplane must not start may be slower no radio wake same contract, different latency
Radios return without resubmit must not flush a queue idle no surprise TLS wait for the user

If any row depends on “the interceptor will 500 anyway,” the gate is not done. Fail closed in the UI instead of waiting for a host error that also proves you woke the radio.

Limitations and who should skip this

This workflow does not measure model quality, tokens per second, or thermal headroom on a given SoC. It also does not replace a privacy review of logcat, bugreports, screenshots, or backup artifacts. Skip it if your app has no on-device path and honestly requires the network for every token. Skip it if you only have an emulator, because you cannot observe radio wake there with confidence. Skip it if a legal or safety flow must reach a server even while offline; that flow needs an explicit online requirement, not a silent fallback.

Do not copy latency numbers from another phone into your onboarding PR. Different compile caches, thermal states, and NNAPI or Core ML backends will move first-token time without changing the airplane-mode contract you are actually shipping.

Ask for comparable evidence, not vibes

Leave the device name, OS build, and the exact transition you ran: airplane during in-flight, airplane then cold prompt, or Low Power Mode combined. Say whether the UI recovered with a local answer, restarted the request, or silently disappeared when radios returned. If cloud traffic still appeared, name the class that opened the socket so the next junior can grep it in the first hour.

Top comments (0)