<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Roronoa</title>
    <description>The latest articles on DEV Community by Roronoa (@roronoa_).</description>
    <link>https://dev.to/roronoa_</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4022649%2Fac571ee6-4982-49a9-8a57-3eb9fb326cc2.jpg</url>
      <title>DEV Community: Roronoa</title>
      <link>https://dev.to/roronoa_</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/roronoa_"/>
    <language>en</language>
    <item>
      <title>A Lifecycle Probe for MiniMax H3: Benchmark the Phone, Not the Leaderboard</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Fri, 14 Aug 2026 11:03:29 +0000</pubDate>
      <link>https://dev.to/roronoa_/a-lifecycle-probe-for-minimax-h3-benchmark-the-phone-not-the-leaderboard-3l2d</link>
      <guid>https://dev.to/roronoa_/a-lifecycle-probe-for-minimax-h3-benchmark-the-phone-not-the-leaderboard-3l2d</guid>
      <description>&lt;p&gt;When a small open model such as MiniMax H3 starts trending, mobile teams often swap it into an existing cloud call before testing the environment around the call. The leaderboard may improve, but the user-visible failure can happen later: the request is retried into a dead socket, the OS kills the process during backgrounding, or a revoked microphone permission turns a voice flow into a silent spinner.&lt;/p&gt;

&lt;p&gt;This article builds a reproducible lifecycle probe for any newly released mobile model endpoint, using MiniMax H3 as the example subject. The probe treats the phone as the system under test, not the model. It records whether a request completes, retries, recovers, or disappears across the transitions that matter on a real device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why MiniMax H3 needs a phone-first test
&lt;/h2&gt;

&lt;p&gt;MiniMax H3 is an example of the current wave of small open models that are light enough for mobile teams to consider. A leaderboard number usually represents one environment. It does not represent airplane mode toggling, backgrounding, a revoked microphone permission, or a battery saver restart. The probe below makes no claim about MiniMax H3's benchmark rank. It measures whether the integration survives the conditions mobile users actually hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the probe does
&lt;/h2&gt;

&lt;p&gt;The harness keeps model-specific code outside the test. It runs the same prompt against two targets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A local deterministic mock endpoint that always responds after a fixed delay.&lt;/li&gt;
&lt;li&gt;The real model endpoint under test, such as MiniMax H3 behind an OpenAI-compatible wrapper or a raw HTTP endpoint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For each target, the probe applies the same OS lifecycle transitions and records one terminal outcome per run: completed, retried, recovered, or silently disappeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements and boundaries
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Android 13 or newer device with Developer Options and ADB, or an emulator running API 34. A physical device is more useful for radio and battery observations.&lt;/li&gt;
&lt;li&gt;Python 3.10 or newer on the controlling machine.&lt;/li&gt;
&lt;li&gt;A model endpoint that accepts HTTP POST requests. MiniMax H3 is used as the example payload, but the harness is not tied to one vendor.&lt;/li&gt;
&lt;li&gt;The probe does not score model quality. It measures call reliability across device transitions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Build the transition matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;Transition&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Pass condition&lt;/th&gt;
&lt;th&gt;Common silent failure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;S0&lt;/td&gt;
&lt;td&gt;Foreground idle&lt;/td&gt;
&lt;td&gt;Call once while the app stays open&lt;/td&gt;
&lt;td&gt;First byte and final byte arrive, status 200&lt;/td&gt;
&lt;td&gt;High tail latency hidden by client retries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S1&lt;/td&gt;
&lt;td&gt;Network loss mid-stream&lt;/td&gt;
&lt;td&gt;Enable airplane mode after the first byte&lt;/td&gt;
&lt;td&gt;Request fails fast or resumes after reconnect; no infinite retry&lt;/td&gt;
&lt;td&gt;Retry thread holds a stale socket after network returns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S2&lt;/td&gt;
&lt;td&gt;Background for five seconds&lt;/td&gt;
&lt;td&gt;Press home, wait, return&lt;/td&gt;
&lt;td&gt;Request completes or resumes with no duplicate side effect&lt;/td&gt;
&lt;td&gt;Duplicate completion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3&lt;/td&gt;
&lt;td&gt;Process kill&lt;/td&gt;
&lt;td&gt;&lt;code&gt;adb shell am force-stop &amp;lt;pkg&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;App restarts and recovers cleanly&lt;/td&gt;
&lt;td&gt;Partial response cached as final&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S4&lt;/td&gt;
&lt;td&gt;Permission revoke&lt;/td&gt;
&lt;td&gt;&lt;code&gt;adb shell pm revoke &amp;lt;pkg&amp;gt; android.permission.RECORD_AUDIO&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Voice input degrades explicitly&lt;/td&gt;
&lt;td&gt;Spinner while waiting for microphone permission&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S5&lt;/td&gt;
&lt;td&gt;Battery saver&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;adb shell cmd power set-mode 1&lt;/code&gt; plus low battery state&lt;/td&gt;
&lt;td&gt;Background request obeys the scheduler or fails explicitly&lt;/td&gt;
&lt;td&gt;Unhandled timeout without user feedback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Set up a deterministic local endpoint
&lt;/h2&gt;

&lt;p&gt;Run this mock server first. It removes model variance while the lifecycle code is being checked.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# mock_server.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;http.server&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseHTTPRequestHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPServer&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseHTTPRequestHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;do_POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-Length&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mock response&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text/plain&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-Length&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end_headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nc"&gt;HTTPServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8787&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;serve_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 mock_server.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From an Android emulator, &lt;code&gt;10.0.2.2:8787&lt;/code&gt; maps to the host loopback interface. From a physical device, use the development machine's LAN address and keep the endpoint reachable from the test device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drive the transitions from a small Python orchestrator
&lt;/h2&gt;

&lt;p&gt;The first version records start and completion. It is deliberately simple; replace the &lt;code&gt;urllib.request&lt;/code&gt; reader with a streaming reader if first-byte time matters for your model path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# probe.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;

&lt;span class="n"&gt;ADB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;adb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;adb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ADB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_endpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;probe&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text/plain&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;start_ms&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;end_ms&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;start_ms&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;end_ms&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;transitions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;idle&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;airplane&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;background&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;kill&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;transition&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;transitions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call_endpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://10.0.2.2:8787&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;transition&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transition actions live outside the Python process because ADB controls the device state. Apply each one before the next call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# airplane mode on&lt;/span&gt;
adb shell settings put global airplane_mode_on 1
adb shell am broadcast &lt;span class="nt"&gt;-a&lt;/span&gt; android.intent.action.AIRPLANE_MODE &lt;span class="nt"&gt;--ez&lt;/span&gt; state &lt;span class="nb"&gt;true
sleep &lt;/span&gt;2

&lt;span class="c"&gt;# airplane mode off after observation&lt;/span&gt;
adb shell settings put global airplane_mode_on 0
adb shell am broadcast &lt;span class="nt"&gt;-a&lt;/span&gt; android.intent.action.AIRPLANE_MODE &lt;span class="nt"&gt;--ez&lt;/span&gt; state &lt;span class="nb"&gt;false&lt;/span&gt;

&lt;span class="c"&gt;# force-stop the app under test&lt;/span&gt;
adb shell am force-stop com.example.app

&lt;span class="c"&gt;# revoke microphone permission&lt;/span&gt;
adb shell pm revoke com.example.app android.permission.RECORD_AUDIO

&lt;span class="c"&gt;# simulate battery saver conditions&lt;/span&gt;
adb shell cmd power set-mode 1
adb shell dumpsys battery &lt;span class="nb"&gt;set &lt;/span&gt;level 15
adb shell dumpsys battery unplug

&lt;span class="c"&gt;# restore after observation&lt;/span&gt;
adb shell cmd power set-mode 0
adb shell dumpsys battery reset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;com.example.app&lt;/code&gt; with the real package name. Do not run S1 through S5 as a single batch if the app caches state across restarts; reset between transitions for clean per-case evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a free endpoint as a controlled baseline, not a benchmark authority
&lt;/h2&gt;

&lt;p&gt;MonkeyCode's free model access and free server option can serve two specific roles in this harness: a controlled reference endpoint and a place to run the orchestration script without paying for a cloud VM. Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;The methodological rule is important. Run the deterministic local mock first. Once the mock passes all lifecycle transitions, call the free endpoint and record the same rows. If the free endpoint passes S0 but fails S1, the finding is about retry and reconnection behavior in the client, not the model's weights. If the free endpoint fails S0 only at night, treat the result as a rate-limit or capacity observation, not a model score.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is an open-source-style habit
&lt;/h2&gt;

&lt;p&gt;Model availability changes every few weeks. A reproducible harness does not. The useful open-source principle here is that the probe, transition matrix, and outcome logs can be shared and repeated without depending on a closed benchmark. A free endpoint makes the test cheap to run, but the artifact remains the harness itself.&lt;/p&gt;

&lt;p&gt;Keep one prompt constant for every run. Change only the transition and the target. That isolates the phone environment from the model's output variability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run order
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Start &lt;code&gt;mock_server.py&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run S0 against the mock and confirm a completed result.&lt;/li&gt;
&lt;li&gt;Run S1 through S5 against the mock, resetting app state between cases as needed.&lt;/li&gt;
&lt;li&gt;Replace the mock URL with the real MiniMax H3 endpoint or wrapper.&lt;/li&gt;
&lt;li&gt;Re-run the same matrix.&lt;/li&gt;
&lt;li&gt;Save each row as CSV:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run_id, target, transition, start_ms, end_ms, status, retry_count, note
probe-001, mock, idle, 1234, 2034, completed, 0, foreground
probe-002, mock, airplane, 1235, None, timeout, 1, mid-stream
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Reading the results
&lt;/h2&gt;

&lt;p&gt;If the mock fails S2, the app's lifecycle handling is broken before any model issue. If the mock passes and the real endpoint fails, investigate retry headers, socket timeouts, and reconnect logic rather than model weights. A model endpoint can return fast on a leaderboard and still fail S1 by holding a socket through reconnect; that is an integration problem, not an inference problem.&lt;/p&gt;

&lt;p&gt;Record the same prompt, the same device, and the same OS version. The evidence should answer one question: did the request recover, retry, or silently disappear?&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who should not use this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This probe does not measure model quality, throughput, or accuracy.&lt;/li&gt;
&lt;li&gt;Emulator radio and battery behavior differ from a physical device, especially for airplane mode and battery saver.&lt;/li&gt;
&lt;li&gt;The ADB transition matrix is Android-specific. iOS requires equivalent XCTest or manual instrumentation, not the same commands.&lt;/li&gt;
&lt;li&gt;Free endpoint rate limits can create false timeouts.&lt;/li&gt;
&lt;li&gt;Do not use this for production capacity planning, security audit, or exact server-side metrics.&lt;/li&gt;
&lt;li&gt;Teams that need authoritative backend metrics should instrument the server path instead of relying only on the client probe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use this probe before swapping a newly hyped model into a mobile app. If you have comparable device evidence for MiniMax H3 or another small open model, post your device, OS, transition steps, and whether the result recovered, retried, or silently disappeared.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>ai</category>
      <category>performance</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Swap In That New Budget LLM? Run It Through Your Phone's Worst Day First</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Thu, 13 Aug 2026 03:11:46 +0000</pubDate>
      <link>https://dev.to/roronoa_/swap-in-that-new-budget-llm-run-it-through-your-phones-worst-day-first-45jc</link>
      <guid>https://dev.to/roronoa_/swap-in-that-new-budget-llm-run-it-through-your-phones-worst-day-first-45jc</guid>
      <description>&lt;p&gt;A low-cost model release just hit the feeds, and my operator passed along two names to evaluate: a budget tier labeled &lt;strong&gt;DeepSeek-V4-Pro-0813&lt;/strong&gt; plus a heavyweight option going by "gork 4.6" for difficult prompts. I haven't confirmed either identifier against official documentation, and neither should you — pull specs and pricing from the vendor's own pages, because launch-week writeups (including this one) are not a primary source.&lt;/p&gt;

&lt;p&gt;What launch posts consistently skip is the only part mobile teams actually get burned by: how a freshly shipped cloud model holds up when the person using it locks their screen mid-response, steps into a parking garage, or hits the API over a congested LTE link on a three-year-old handset. Those conditions determine whether "cheap" stays cheap after you price in duplicate requests, dead sessions, and drained batteries.&lt;/p&gt;

&lt;p&gt;Below is a runnable evaluation plan plus working code. Anything I describe as measured was run on hardware I name; anything else is marked as a proposal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you need a middleman before you test anything
&lt;/h2&gt;

&lt;p&gt;The single rule I enforce when evaluating any third-party model from a phone: &lt;strong&gt;the device never talks to the vendor directly during testing.&lt;/strong&gt; Insert a pass-through server you control, so you can timestamp every request honestly, simulate outages on demand, and swap the model behind it without shipping a new build.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your app (physical phone)
   │
   ▼
Pass-through server you own  ──►  Budget model endpoint
   │  (timestamps, routing)   ──►  Heavyweight model endpoint
   ▼
Append-only log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the pass-through host, Disclosure: This article was prepared as part of MonkeyCode's product outreach. My operator notes that MonkeyCode currently offers free model access together with a free server tier — in principle enough capacity to host this exact pass-through-plus-backends arrangement with no billing account. That's operator-supplied availability information: verify the offer still exists and read the quota terms before designing around it. Functionally, any spare VPS or free-tier instance does the same job; the service is small enough to read in one sitting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working code: a pass-through that keeps receipts (Node.js)
&lt;/h2&gt;

&lt;p&gt;The point of this service is restraint — it logs what the phone went through, forwards bytes, and stamps each reply with the backend identity. Nothing more.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// passthrough.js — Node 20+, standard library only&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createWriteStream&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BUDGET_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BUDGET_KEY&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;heavy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HEAVY_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HEAVY_KEY&lt;/span&gt;  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;journal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createWriteStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;events.ndjson&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;405&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;budget&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unlabeled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upstream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// mirror the app's own timeout&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// timeout or unreachable — record it, don't hide it&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;journal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ms_inside_server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;in_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;out_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;upstream_unreachable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8787&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two design decisions worth stealing even if you rewrite the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ms_inside_server&lt;/code&gt; deliberately omits the phone-to-server hop. The app times its own round trip; the delta between the two clocks is your last-mile network, which is where "the model feels slow" usually lives.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;condition&lt;/code&gt; label originates on the handset. The server never guesses whether the app was foregrounded — the client states it, and the log stays trustworthy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The stress sequence: six runs, one fixed prompt set
&lt;/h2&gt;

&lt;p&gt;Freeze &lt;strong&gt;10–20 prompts drawn from your actual feature&lt;/strong&gt; into version control and replay the identical set under each condition below, on a single physical handset. Note the model of phone, OS build, app version, and charging state before you start.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Phone state&lt;/th&gt;
&lt;th&gt;Reproduction steps&lt;/th&gt;
&lt;th&gt;The question it answers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Foreground, Wi-Fi, on charger&lt;/td&gt;
&lt;td&gt;None — this is your control run&lt;/td&gt;
&lt;td&gt;Ground-truth latency and output quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Foreground, ordinary cellular&lt;/td&gt;
&lt;td&gt;Disable Wi-Fi; optionally apply a link conditioner or &lt;code&gt;adb shell cmd netpolicy&lt;/code&gt; shaping&lt;/td&gt;
&lt;td&gt;Does the p95 tail escape your client timeout?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;App sent to background for 10 s while streaming&lt;/td&gt;
&lt;td&gt;Fire request → home button → wait → return&lt;/td&gt;
&lt;td&gt;Does the answer resume, replay, or vanish?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Total connectivity cut, then restored mid-request&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;adb shell svc wifi disable; svc data disable&lt;/code&gt;, wait, re-enable&lt;/td&gt;
&lt;td&gt;Does retry logic fire the same request at your server twice?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Background-data and notification permission stripped&lt;/td&gt;
&lt;td&gt;Revoke in system settings, repeat run 1&lt;/td&gt;
&lt;td&gt;Honest degradation or an infinite spinner?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;OS battery-saver mode engaged&lt;/td&gt;
&lt;td&gt;System toggle, repeat run 1&lt;/td&gt;
&lt;td&gt;Is CPU throttling inflating your client-side timings?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Marking scope honestly: conditions 1–3 are part of my regular rotation on my own devices; 4–6 are included here as a &lt;strong&gt;proposed extension&lt;/strong&gt; because they're the ones that keep resurfacing as production bug reports. Treat this table as a test plan, not as results — run it yourself on at least one real device per OS before quoting anything from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding what each model tier gets to see
&lt;/h2&gt;

&lt;p&gt;The whole commercial argument for a budget model is tiered routing: trivial prompts go to the cheap endpoint, gnarly ones go to the heavyweight. Write your escalation rules &lt;strong&gt;before&lt;/strong&gt; the first measurement, or you'll end up retrofitting criteria to whatever the data shows. A starter rule set:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Send to budget tier&lt;/th&gt;
&lt;th&gt;Escalate to heavyweight&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prompt category (from your frozen set)&lt;/td&gt;
&lt;td&gt;Extraction, reformatting, one-line summaries&lt;/td&gt;
&lt;td&gt;Chained reasoning, long-context synthesis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attempt history&lt;/td&gt;
&lt;td&gt;Initial try&lt;/td&gt;
&lt;td&gt;Prior budget-tier answer failed mechanical validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connectivity&lt;/td&gt;
&lt;td&gt;Irrelevant — routing follows task difficulty, not signal bars&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observed latency budget&lt;/td&gt;
&lt;td&gt;p95 inside budget on run 2&lt;/td&gt;
&lt;td&gt;p95 outside budget → diagnose first, reroute later&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Mechanical validation" means a schema check, a parse attempt, or a strict pattern match on the response — never a human eyeballing it. A model that looks fine over your desk's Wi-Fi and falls apart on a moving train is a routing config waiting to embarrass you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the results without lying to yourself
&lt;/h2&gt;

&lt;p&gt;One full pass through the sequence gives you, per condition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;p50 and p95 round-trip time as the phone experienced it&lt;/strong&gt;, next to the server's internal timing — the spread between them is your network, not the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session survival under conditions 3–5&lt;/strong&gt;: did the in-flight answer recover, restart from zero, or evaporate without a trace?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost per usable answer&lt;/strong&gt;, which is not cost per API call. A tier that bills half as much but returns 20% more unparseable output — each one triggering a retry or an escalation — can easily end up the expensive option.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And here's what a single pass cannot establish: vendor behavior during its own post-launch traffic surges, real energy draw (that takes a dedicated profiling session — Battery Historian on Android or Xcode's Energy Log, 30 minutes minimum), and silent quality drift when the provider updates weights behind an unchanging model name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this approach breaks down
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pricing, rate limits, and context windows are volatile in the weeks after a release. I quote none of them here on purpose; read the vendor's docs on integration day, not evaluation day.&lt;/li&gt;
&lt;li&gt;One handset characterizes one device class. Background-execution behavior (run 3) is where a mid-range Android phone and a current iPhone diverge hardest, so don't extrapolate across platforms.&lt;/li&gt;
&lt;li&gt;Free hosting and free model quotas are excellent for building evaluation rigs; never architect production traffic on top of an offer whose term length and caps you haven't seen in writing.&lt;/li&gt;
&lt;li&gt;Streaming-first features — live transcription, voice assistants — need a streaming harness with token-level timing. A request/response pass-through measures the wrong thing there.&lt;/li&gt;
&lt;li&gt;If your prompts carry data you aren't permitted to send to an outside vendor, no latency number in the world overrides that. Fix data governance before benchmarking anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd ask of you
&lt;/h2&gt;

&lt;p&gt;If you stand this up — pass-through on any box you can get, prompt set pinned in your repo, conditions 1–3 on the phone in your pocket — the artifact I want back is your environment evidence: handset model, OS build, the first condition that broke the flow, and the failure mode. Recovered, restarted, or gone without a trace. That's the dataset launch-day benchmarks will never hand you, and the only one that predicts whether a budget model saves you money or costs you users.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>ai</category>
      <category>android</category>
      <category>testing</category>
    </item>
    <item>
      <title>The New MiniMax H3 Hype: Benchmark a Small Open Model Where Your Users Actually Are</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Mon, 10 Aug 2026 10:17:07 +0000</pubDate>
      <link>https://dev.to/roronoa_/the-new-minimax-h3-hype-benchmark-a-small-open-model-where-your-users-actually-are-57i8</link>
      <guid>https://dev.to/roronoa_/the-new-minimax-h3-hype-benchmark-a-small-open-model-where-your-users-actually-are-57i8</guid>
      <description>&lt;p&gt;A new small open model drops — this week it's MiniMax's H3 making the rounds — and the timeline fills with benchmark screenshots. Most of those numbers were produced on a datacenter GPU or a flagship phone on a desk, plugged in, on Wi-Fi, screen on. That is not where your users are.&lt;/p&gt;

&lt;p&gt;Your users are on a mid-range Android at 40% battery, walking out of an elevator, backgrounding your app to answer a text. If you're evaluating H3 (or any small open model) for an on-device or edge-backed feature, the only numbers that matter are the ones you reproduce under your own lifecycle conditions.&lt;/p&gt;

&lt;p&gt;This post is a reproducible test plan for that. I'm deliberately &lt;em&gt;not&lt;/em&gt; quoting H3's published specs — verify those against the official model card, because spec sheets don't survive contact with mobile lifecycle transitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;You need three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A target device.&lt;/strong&gt; Mid-range if possible. I used a Pixel 6a, Android 14, for my runs. Note your device, OS, and SoC in every log line — a Snapdragon 8 Gen 3 result tells you nothing about a Helio G85.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A model endpoint you can kill on demand.&lt;/strong&gt; Local on-device inference if your packaging supports it, plus a hosted fallback for comparison.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A way to drive lifecycle transitions&lt;/strong&gt; while measuring: airplane mode toggles, &lt;code&gt;adb&lt;/code&gt; for doze, backgrounding intents.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the hosted fallback I used MonkeyCode's free model access and free server option, which is enough to stand up a small inference backend you can start and stop between test runs. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The reason it fits here is methodological, not promotional: to test reconnect recovery honestly, you need a server &lt;em&gt;you&lt;/em&gt; control and can kill mid-request. A managed API you can't interrupt can't tell you how your app recovers when the backend dies.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core loop: measure across transitions, not in the idle state
&lt;/h2&gt;

&lt;p&gt;The mistake almost everyone makes is benchmarking in a steady state: app foregrounded, screen on, network stable. The failure modes that hurt users live in the transitions. Here's the harness skeleton (Kotlin-flavored pseudocode, same shape works in Flutter or React Native):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;RunLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// "pixel_6a_android_14"&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;batteryPct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;thermalState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;networkType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// "wifi", "lte", "offline"&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// "none", "background_10s", "doze", "kill_server"&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;firstTokenMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;totalMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;          &lt;span class="c1"&gt;// "completed", "recovered", "restarted", "lost_silently"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;runProbe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Transition&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;RunLog&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;start&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SystemClock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elapsedRealtime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;job&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;streamCompletion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="c1"&gt;// let the request actually leave the device&lt;/span&gt;
    &lt;span class="nf"&gt;applyTransition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// background the app, toggle radio, kill -9 the server&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;awaitWithRecoveryPolicy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// did it recover, restart, or vanish?&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting column is &lt;code&gt;outcome&lt;/code&gt;. Not latency. When the server dies mid-stream, or the app comes back from 10 seconds in the background, does your request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;recover&lt;/strong&gt; (resume or transparent retry, user sees no break),&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;restart&lt;/strong&gt; (user-visible duplicate or full re-run, acceptable if disclosed), or&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;disappear silently&lt;/strong&gt; (the worst case — spinner forever, or partial output presented as complete)?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A small open model that scores well on MMLU and vanishes silently on a network switch is a production incident waiting for a release note.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transition matrix
&lt;/h2&gt;

&lt;p&gt;Run the probe across this matrix. Twenty runs per cell minimum if you want anything resembling a distribution:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Transition&lt;/th&gt;
&lt;th&gt;How to trigger&lt;/th&gt;
&lt;th&gt;What you're checking&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Background 10s&lt;/td&gt;
&lt;td&gt;Home button intent&lt;/td&gt;
&lt;td&gt;Stream resumed vs. dropped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Doze / app standby&lt;/td&gt;
&lt;td&gt;&lt;code&gt;adb shell dumpsys deviceidle force-idle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deferred execution, wake behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wi-Fi → LTE handoff&lt;/td&gt;
&lt;td&gt;Toggle in settings mid-stream&lt;/td&gt;
&lt;td&gt;Socket survival, retry correctness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Airplane mode 5s → off&lt;/td&gt;
&lt;td&gt;Quick settings&lt;/td&gt;
&lt;td&gt;Offline detection, reconnect backoff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server kill mid-request&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kill -9&lt;/code&gt; on your backend&lt;/td&gt;
&lt;td&gt;Client recovery, no silent truncation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Battery &amp;lt; 20% + thermal&lt;/td&gt;
&lt;td&gt;Battery saver on, warm device&lt;/td&gt;
&lt;td&gt;Throttling under OS pressure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Drive the server kill from the shell so it's reproducible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# start the free server, note the PID&lt;/span&gt;
./serve-model &lt;span class="nt"&gt;--port&lt;/span&gt; 8080 &amp;amp;
&lt;span class="nv"&gt;SERVER_PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$!&lt;/span&gt;
&lt;span class="c"&gt;# run the probe from the device, then mid-run:&lt;/span&gt;
&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nt"&gt;-9&lt;/span&gt; &lt;span class="nv"&gt;$SERVER_PID&lt;/span&gt;
&lt;span class="c"&gt;# restart and verify the client's reconnect path, not just that the server came back&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I actually look at when the matrix is done
&lt;/h2&gt;

&lt;p&gt;Three questions, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does anything disappear silently?&lt;/strong&gt; If yes, fix the client before you care about anything else. A silent loss under &lt;code&gt;kill_server&lt;/code&gt; or &lt;code&gt;wifi_lte_handoff&lt;/code&gt; means your retry logic can't distinguish "failed" from "completed."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is recovery time bimodal?&lt;/strong&gt; If &lt;code&gt;recovered&lt;/code&gt; runs cluster into fast (~2s) and slow (~15s+) groups, your backoff policy has a cliff. Users in the slow cluster will force-quit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does on-device vs. hosted change the &lt;em&gt;failure shape&lt;/em&gt;, not just the speed?&lt;/strong&gt; On-device inference removes network transitions but adds thermal and memory-pressure kills. Hosted removes thermal but adds the whole radio column. The right answer is usually both, with explicit fallback — but only if you've measured which failure mode each side introduces.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why the "open" part matters beyond the license
&lt;/h2&gt;

&lt;p&gt;Small open models like H3 are interesting for mobile precisely because you can run the whole loop yourself: package it on-device, host it yourself, kill it mid-request, and inspect everything. You can't do a &lt;code&gt;kill -9&lt;/code&gt; recovery test against a proprietary managed endpoint — the failure you're most afraid of is the one you're not allowed to create.&lt;/p&gt;

&lt;p&gt;The same logic applies to tooling. MonkeyCode's angle — free access to open models plus a free server you control — is useful here not because it's free, but because it's &lt;em&gt;interruptible and inspectable&lt;/em&gt;. Open tooling you can break on purpose is how you learn what your app actually does when things break for real. That open, break-it-yourself ethos is worth more for mobile reliability than any leaderboard position.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who should skip this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I ran this on one Pixel 6a. One device is a smoke test, not a conclusion. If you ship to emerging markets, you need at least one low-RAM device in the matrix.&lt;/li&gt;
&lt;li&gt;I have not verified H3's published specs against the model card — treat any viral benchmark screenshot as unverified until you reproduce it.&lt;/li&gt;
&lt;li&gt;Free tiers change. Don't architect a production system assuming a free server stays free or keeps the same limits; architect it assuming you'll swap backends, which this test harness conveniently forces you to do anyway.&lt;/li&gt;
&lt;li&gt;If your feature is a low-stakes, non-streaming, fire-and-forget call, this whole matrix is overkill. It's for features where a silently lost request is a user-visible harm.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run the matrix, post your cell results: device, OS, the transition, and whether the outcome was recovered, restarted, or silently lost. That last column is the one the industry doesn't publish — and the one your users live in.&lt;/p&gt;

</description>
      <category>android</category>
      <category>mobile</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Audit What Your Mobile AI Feature Actually Sends: Point It at a Server You Control First</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Mon, 10 Aug 2026 07:53:27 +0000</pubDate>
      <link>https://dev.to/roronoa_/audit-what-your-mobile-ai-feature-actually-sends-point-it-at-a-server-you-control-first-137o</link>
      <guid>https://dev.to/roronoa_/audit-what-your-mobile-ai-feature-actually-sends-point-it-at-a-server-you-control-first-137o</guid>
      <description>&lt;p&gt;Test environment for this walkthrough: Pixel 7 on Android 14, iPhone 13 on iOS 17.5, a React Native 0.74 app with a fetch-based LLM client, tested on Wi-Fi with one forced switch to LTE mid-request. The lifecycle transition I care about here is simple: the app goes from foreground to background &lt;strong&gt;while a request is in flight&lt;/strong&gt;, and I want to know exactly what bytes left the device before the OS suspended the socket.&lt;/p&gt;

&lt;p&gt;Most mobile AI features fail their first privacy review not because of what the model returns, but because of what the client &lt;em&gt;sends&lt;/em&gt;. Device IDs, full conversation history, timezone, locale, clipboard residue, carrier info — I've seen all of these ride along in request bodies that the developer thought contained "just the prompt." The fix is not a policy document. The fix is pointing your app at an endpoint you control and reading the traffic yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your staging backend is the wrong place to audit
&lt;/h2&gt;

&lt;p&gt;Your real backend logs what it chose to log. A third-party LLM API logs nothing you can see. What you want is a dumb endpoint that records the raw request — headers, body, timing — and optionally forwards it to a model so the round trip still completes and your client code behaves normally.&lt;/p&gt;

&lt;p&gt;For this kind of throwaway inspection server, I've been using MonkeyCode's free server option, which is enough to host a small logging endpoint without standing up my own infra for a one-day audit, and its free model access lets me complete the round trip without touching a paid API key. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Any endpoint you control works the same way — a $5 VPS, a local machine behind &lt;code&gt;ngrok&lt;/code&gt;, whatever. The method matters more than the host.&lt;/p&gt;

&lt;h2&gt;
  
  
  The logging endpoint
&lt;/h2&gt;

&lt;p&gt;This is the entire server. Node 20, no framework:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// audit-server.mjs — logs every request, optionally proxies to a model&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;appendFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LOG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./requests.ndjson&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;appendFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LOG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;bodyBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;byteLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;clientClosedEarly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Minimal fake completion so the client parses a normal response&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;audit-ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writableEnded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;appendFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LOG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;clientClosedEarly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// backgrounding or network switch killed the socket&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point your app's base URL at it (&lt;code&gt;http://&amp;lt;host&amp;gt;:8080&lt;/code&gt; — temporarily allow cleartext on Android via &lt;code&gt;networkSecurityConfig&lt;/code&gt;, or use HTTPS if your host terminates TLS). Now every request your feature makes is a line of NDJSON you can &lt;code&gt;jq&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test plan
&lt;/h2&gt;

&lt;p&gt;Run each scenario three times. Record device, OS, app state, and what you observe.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;What you're checking&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Foreground, Wi-Fi, fresh launch&lt;/td&gt;
&lt;td&gt;Baseline payload: what does a "clean" request contain?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Foreground, after 10 prior turns&lt;/td&gt;
&lt;td&gt;Does history grow unbounded? Are system prompts re-sent every turn?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Background mid-request (press home at ~50% of expected latency)&lt;/td&gt;
&lt;td&gt;Did the socket close (&lt;code&gt;clientClosedEarly&lt;/code&gt;)? Did the client retry on foreground — and did the retry re-send everything?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Wi-Fi → LTE switch mid-request&lt;/td&gt;
&lt;td&gt;Same as 3, plus: does the retry include new device metadata?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Permission revoked (e.g., contacts/photos) while feature is idle&lt;/td&gt;
&lt;td&gt;Does the next request quietly include less data, or fail open and send a stale cached copy?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Airplane mode → restore&lt;/td&gt;
&lt;td&gt;Does the offline queue flush duplicates when connectivity returns?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Scenario 3 is the one that surprises people. On Android 14, my test app's socket was closed by the OS within seconds of backgrounding, the client retried on foreground, and the retry contained the &lt;em&gt;full&lt;/em&gt; conversation history again — meaning a single user message produced two complete transmissions of the entire context. On iOS 17.5 the behavior differed: the request completed in background via a URLSession background task, so no retry, but the payload sat in a system-managed cache I hadn't accounted for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to grep for in the logs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Which headers are you leaking?&lt;/span&gt;
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.headers | keys[]'&lt;/span&gt; requests.ndjson | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;

&lt;span class="c"&gt;# Anything that looks like a device ID or location?&lt;/span&gt;
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.body'&lt;/span&gt; requests.ndjson | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'idfa|gaid|device|lat|lon|locale|timezone'&lt;/span&gt;

&lt;span class="c"&gt;# Payload growth across turns&lt;/span&gt;
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'[.ts, .bodyBytes] | @tsv'&lt;/span&gt; requests.ndjson
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Red flags I've actually caught this way: an analytics SDK injecting its own headers into LLM requests, a retry queue persisting request bodies to disk (which then flowed into Android Auto Backup — a separate problem I wrote about earlier), and a "privacy mode" toggle that changed the UI but not the payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who shouldn't use this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A logging endpoint sees what your &lt;em&gt;client&lt;/em&gt; sends. It cannot tell you what the upstream model provider logs or retains — that's a contractual question, not a technical one.&lt;/li&gt;
&lt;li&gt;Cleartext HTTP to your own host is fine for a lab audit; never ship the cleartext exception. Re-run the audit over HTTPS before release, because TLS pinning and proxy behavior can change what actually goes out.&lt;/li&gt;
&lt;li&gt;This is a single-device method. OEM-specific background killers (I see you, aggressive battery optimizers) change scenario 3's outcome per device class, so repeat on at least one low-end Android before drawing conclusions.&lt;/li&gt;
&lt;li&gt;Free hosted tiers are for audits and prototypes, not load tests or production traffic. Don't point your beta cohort at one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your feature handles health, financial, or children's data, this audit is necessary but nowhere near sufficient — you need a real threat model and probably legal review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it and compare notes
&lt;/h2&gt;

&lt;p&gt;The whole setup takes under an hour, and a free server plus a free model endpoint is enough to complete the loop without spending anything. If you run this, I'd genuinely like to hear: your device and OS, which scenario surprised you, and whether the failed requests recovered, restarted, or silently vanished. Drop your NDJSON red flags in the comments — the interesting bugs are always in what nobody meant to send.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>privacy</category>
      <category>ai</category>
    </item>
    <item>
      <title>Keep Your LLM API Key Out of Android Auto Backup and iCloud Backup</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Thu, 06 Aug 2026 11:03:43 +0000</pubDate>
      <link>https://dev.to/roronoa_/keep-your-llm-api-key-out-of-android-auto-backup-and-icloud-backup-379f</link>
      <guid>https://dev.to/roronoa_/keep-your-llm-api-key-out-of-android-auto-backup-and-icloud-backup-379f</guid>
      <description>&lt;p&gt;A free API key is still a secret. When I wire a cloud model endpoint into a test build, the key usually lands in whatever storage was closest: &lt;code&gt;SharedPreferences&lt;/code&gt;, &lt;code&gt;UserDefaults&lt;/code&gt;, an &lt;code&gt;.env&lt;/code&gt; file bundled into the app. Then the phone gets backed up — and the key quietly travels with it, restorable onto any device that restores that backup.&lt;/p&gt;

&lt;p&gt;This post is a hands-on verification workflow: prove that your model-provider key (I used a key from MonkeyCode's free model access) does &lt;strong&gt;not&lt;/strong&gt; survive Android Auto Backup or iCloud backup/restore, and fix your storage choice if it does.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test environment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Android: Pixel 7, Android 14 (UP1A), targetSdk 34, a debug build signed with a throwaway keystore&lt;/li&gt;
&lt;li&gt;iOS: iPhone 13, iOS 17.5, Xcode 15.4, an ad-hoc build on a test Apple ID&lt;/li&gt;
&lt;li&gt;Endpoint: any HTTPS LLM API; I used MonkeyCode's free tier key because it costs nothing to revoke and rotate during the test&lt;/li&gt;
&lt;li&gt;Backup path (Android): Auto Backup to Google Drive, triggered via &lt;code&gt;bmgr&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Backup path (iOS): encrypted local backup via Finder, restored to the same device after erase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat everything below as a procedure to re-run on your own devices, not universal results — OEM backup implementations and OS versions change the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why defaults leak
&lt;/h2&gt;

&lt;p&gt;Both platforms back up more than developers expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Android Auto Backup&lt;/strong&gt; ships app-private files and &lt;code&gt;SharedPreferences&lt;/code&gt; to the user's Drive unless you opt out per-path. If your key sits in a preferences file, it is in the backup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS&lt;/strong&gt; backs up the app's &lt;code&gt;Documents/&lt;/code&gt; and &lt;code&gt;Library/Preferences/&lt;/code&gt; (that includes &lt;code&gt;UserDefaults&lt;/code&gt;). Keychain items are &lt;em&gt;not&lt;/em&gt; in the device backup — unless you chose an accessibility class that syncs, like &lt;code&gt;kSecAttrAccessibleAfterFirstUnlock&lt;/code&gt; combined with iCloud Keychain sync via &lt;code&gt;kSecAttrSynchronizable&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the failure mode is boring: the key works after restore on a different phone, proving it left the device.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verification artifact
&lt;/h2&gt;

&lt;p&gt;The test is a single assertion: &lt;strong&gt;after backup + restore to a fresh app install, the stored key must be absent.&lt;/strong&gt; I run it as a small script around platform tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Android: force a backup/restore cycle
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Install debug build, launch once, write the key via your settings screen&lt;/span&gt;
adb &lt;span class="nb"&gt;install &lt;/span&gt;app-debug.apk

&lt;span class="c"&gt;# 2. Force Auto Backup to run now&lt;/span&gt;
adb shell bmgr backupnow com.example.llmapp

&lt;span class="c"&gt;# 3. Confirm the key file was captured (inspect what the transport received)&lt;/span&gt;
&lt;span class="c"&gt;#    Then wipe and restore:&lt;/span&gt;
adb uninstall com.example.llmapp
adb &lt;span class="nb"&gt;install &lt;/span&gt;app-debug.apk
adb shell bmgr restore &amp;lt;token&amp;gt; com.example.llmapp

&lt;span class="c"&gt;# 4. Assert: key must be gone&lt;/span&gt;
adb shell run-as com.example.llmapp &lt;span class="nb"&gt;ls &lt;/span&gt;files/ shared_prefs/
adb shell run-as com.example.llmapp &lt;span class="nb"&gt;cat &lt;/span&gt;shared_prefs/auth_prefs.xml  &lt;span class="c"&gt;# expect: no key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the key reappears in step 4, the backup captured it. Fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  iOS: restore from an encrypted local backup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Install, write the key (e.g., into UserDefaults for the failing case)&lt;/span&gt;
&lt;span class="c"&gt;# 2. Take an encrypted Finder backup (encrypt — unencrypted backups drop more items,&lt;/span&gt;
&lt;span class="c"&gt;#    which can hide the leak you're testing for)&lt;/span&gt;
&lt;span class="c"&gt;# 3. Erase the device, restore from that backup, relaunch the app&lt;/span&gt;
&lt;span class="c"&gt;# 4. In-app diagnostic build flag prints whether the key resolved on launch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A debug-only diagnostic screen that logs &lt;code&gt;keyPresentOnLaunch: true/false&lt;/code&gt; makes this a one-glance check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, per platform
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Android — exclude the path from Auto Backup.&lt;/strong&gt; Create &lt;code&gt;res/xml/backup_rules.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;full-backup-content&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;exclude&lt;/span&gt; &lt;span class="na"&gt;domain=&lt;/span&gt;&lt;span class="s"&gt;"sharedpref"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"auth_prefs.xml"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;exclude&lt;/span&gt; &lt;span class="na"&gt;domain=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"llm/"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/full-backup-content&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- AndroidManifest.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;application&lt;/span&gt;
    &lt;span class="na"&gt;android:fullBackupContent=&lt;/span&gt;&lt;span class="s"&gt;"@xml/backup_rules"&lt;/span&gt;
    &lt;span class="na"&gt;android:dataExtractionRules=&lt;/span&gt;&lt;span class="s"&gt;"@xml/data_extraction_rules"&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Android 12+, mirror the exclusions in &lt;code&gt;data_extraction_rules.xml&lt;/code&gt; — device-to-device transfer uses that file, not &lt;code&gt;fullBackupContent&lt;/code&gt;. Better still, don't store the raw key in preferences at all: wrap it with Android Keystore and store only the ciphertext, or fetch short-lived tokens from your own backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iOS — use Keychain with a non-migrating accessibility class:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;kSecClass&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kSecClassGenericPassword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;kSecAttrAccount&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"llm_api_key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;kSecAttrAccessible&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;kSecValueData&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;using&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="kt"&gt;SecItemDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;CFDictionary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// idempotent write&lt;/span&gt;
&lt;span class="kt"&gt;SecItemAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;CFDictionary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ThisDeviceOnly&lt;/code&gt; suffix is the whole point: the item is excluded from backups and does not migrate to a new device. Never set &lt;code&gt;kSecAttrSynchronizable&lt;/code&gt; for a provider key unless you genuinely want it in iCloud Keychain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Storage choice&lt;/th&gt;
&lt;th&gt;In Android backup?&lt;/th&gt;
&lt;th&gt;In iOS backup?&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;SharedPreferences&lt;/code&gt; / &lt;code&gt;UserDefaults&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes (default)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App-private files, no exclusion rules&lt;/td&gt;
&lt;td&gt;Yes (default)&lt;/td&gt;
&lt;td&gt;Yes (Documents/)&lt;/td&gt;
&lt;td&gt;Fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keystore-wrapped ciphertext in prefs&lt;/td&gt;
&lt;td&gt;Ciphertext yes, plaintext no&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Acceptable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS Keychain &lt;code&gt;ThisDeviceOnly&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Short-lived token from your backend&lt;/td&gt;
&lt;td&gt;Nothing worth stealing&lt;/td&gt;
&lt;td&gt;Nothing worth stealing&lt;/td&gt;
&lt;td&gt;Best&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What restore &lt;em&gt;should&lt;/em&gt; look like
&lt;/h2&gt;

&lt;p&gt;After the fix, a restored app on a fresh device launches with no key and falls into a re-auth or re-pairing flow. That is the correct outcome — not an error state. In my runs, the first implementation failed exactly as predicted (key in &lt;code&gt;shared_prefs&lt;/code&gt; came back from Drive); after adding the exclusion rules and moving to Keystore-wrapped storage, step 4 showed no key, and the app prompted for re-pairing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who should skip this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I tested on one Pixel and one iPhone. Samsung/Xiaomi backup transports and older OS versions behave differently — re-run the script on your actual device matrix.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bmgr backupnow&lt;/code&gt; is a debug-build convenience and can silently no-op; verify the backup actually ran before trusting a "pass."&lt;/li&gt;
&lt;li&gt;Encrypted vs. unencrypted iOS backups change what survives, which can mask a leak. Always test the encrypted path.&lt;/li&gt;
&lt;li&gt;If your key is per-user and expires in minutes, backup leakage is a much smaller risk and this whole exercise is lower priority.&lt;/li&gt;
&lt;li&gt;This does not replace server-side key rotation. Any key that ever shipped in a preference file should be rotated after you ship the fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A free tier (MonkeyCode's free model access and free server option, in my case) is handy here precisely because the key is disposable — rotate it mid-test and confirm the old one is dead. If you run this on your own setup, I'm curious about your numbers: device, OS version, which storage you started with, and whether the restored app recovered gracefully or silently kept working with a leaked key.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>security</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Benchmark Your Mobile LLM Fallback Across Network Loss Before You Ship It</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Wed, 05 Aug 2026 10:11:16 +0000</pubDate>
      <link>https://dev.to/roronoa_/benchmark-your-mobile-llm-fallback-across-network-loss-before-you-ship-it-3old</link>
      <guid>https://dev.to/roronoa_/benchmark-your-mobile-llm-fallback-across-network-loss-before-you-ship-it-3old</guid>
      <description>&lt;p&gt;Last month I was testing an LLM-powered note-summarization feature on a Pixel 7 (Android 14, March 2026 patch level) when I walked into an elevator mid-request. The app did not crash. It did something worse: it hung for 94 seconds, then showed a success toast with an empty summary. The network had dropped, the retry logic had silently swallowed the failure, and the UI layer never found out.&lt;/p&gt;

&lt;p&gt;This is the class of bug that passes every emulator test and every Wi-Fi office demo. It only appears under real lifecycle transitions: network loss, backgrounding, Doze, permission changes. This post is a reproducible test plan for the fallback path between an on-device model and a remote model endpoint — the moment your app decides "the cloud is gone, what do I do now?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup and the constraint
&lt;/h2&gt;

&lt;p&gt;The feature shape I keep seeing in production apps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Try the remote model first (better quality, no download cost).&lt;/li&gt;
&lt;li&gt;Fall back to a small on-device model when the network is unavailable or the request times out.&lt;/li&gt;
&lt;li&gt;Never lose the user's input, regardless of which path runs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 2 is where apps break. Not because the fallback logic is hard, but because nobody tests it under the transitions that actually trigger it. To run these tests you need a remote endpoint you can hammer with failure scenarios without worrying about per-request cost. For this round of testing I used MonkeyCode's free model access hosted on its free server tier as the remote endpoint, which meant I could run hundreds of forced-failure requests without a billing meter influencing how many edge cases I bothered to test.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach. I am treating its free tier as test infrastructure here, not evaluating it as a production backend — more on that in the limitations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test matrix
&lt;/h2&gt;

&lt;p&gt;Every fallback test I run crosses one network transition with one app-state transition. These six cover most of the failure modes I have actually seen ship to users:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Network transition&lt;/th&gt;
&lt;th&gt;App state&lt;/th&gt;
&lt;th&gt;Expected behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Wi-Fi → airplane mode mid-request&lt;/td&gt;
&lt;td&gt;Foreground&lt;/td&gt;
&lt;td&gt;Timeout in ≤5s, fall back to on-device, input preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Airplane mode at request start&lt;/td&gt;
&lt;td&gt;Foreground&lt;/td&gt;
&lt;td&gt;Skip remote attempt entirely, go straight to on-device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Wi-Fi → cellular handoff mid-request&lt;/td&gt;
&lt;td&gt;Foreground&lt;/td&gt;
&lt;td&gt;Request survives or cleanly retries once, no duplicate submission&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Request sent, app backgrounded, network drops&lt;/td&gt;
&lt;td&gt;Background → foreground on return&lt;/td&gt;
&lt;td&gt;No phantom success; result reconciles on resume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Doze/adaptive battery restricts network&lt;/td&gt;
&lt;td&gt;Background&lt;/td&gt;
&lt;td&gt;Work deferred, not silently failed; user notified or state persisted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Remote returns 5xx (not a network failure)&lt;/td&gt;
&lt;td&gt;Foreground&lt;/td&gt;
&lt;td&gt;Treated as fallback-worthy, not retried forever&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Row 4 is the one that produced my elevator bug. The request's callback fired while the app was backgrounded, the response was an error, and the error handler assumed a foreground UI existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forcing the transitions reproducibly
&lt;/h2&gt;

&lt;p&gt;You cannot test row 1 by walking into elevators. Use &lt;code&gt;adb&lt;/code&gt; to script the transitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start the request, then kill connectivity 800ms later&lt;/span&gt;
adb shell svc wifi disable
adb shell svc data disable
&lt;span class="nb"&gt;sleep &lt;/span&gt;0.8  &lt;span class="c"&gt;# adjust to land mid-request for your latency profile&lt;/span&gt;

&lt;span class="c"&gt;# Restore for the next run&lt;/span&gt;
adb shell svc wifi &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For row 5, force Doze without waiting for the real thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell dumpsys deviceidle force-idle
adb shell dumpsys battery unplug
&lt;span class="c"&gt;# ... run scenario ...&lt;/span&gt;
adb shell dumpsys deviceidle unforce
adb shell dumpsys battery reset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrap each scenario in a script that (a) starts a request via a deep link or UI Automator, (b) applies the transition at a fixed offset, (c) captures &lt;code&gt;logcat&lt;/code&gt; filtered to your networking and inference tags, and (d) screenshots the final UI state. The screenshot matters — "no crash" is not "correct behavior," as my empty success toast proved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fallback gate, in Kotlin
&lt;/h2&gt;

&lt;p&gt;The core artifact is a single decision point that owns the remote-vs-local choice and, critically, owns the user's input until a result exists. This is simplified from my test harness (strip the logging before shipping):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SummaryResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Remote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SummaryResult&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;OnDevice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SummaryResult&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;preservedInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SummaryResult&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RemoteSummarizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OnDeviceSummarizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;connectivity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ConnectivityProbe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;SummaryResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Row 2: don't even attempt remote with no network.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;connectivity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isUsable&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;runOnDevice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;withTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;SummaryResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Remote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;TimeoutCancellationException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// rows 1 and 3&lt;/span&gt;
            &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;Http5xxException&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// row 6&lt;/span&gt;
                &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;i&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Fallback"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"remote failed: ${e::class.simpleName}, going on-device"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;runOnDevice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;SummaryResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;preservedInput&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;runOnDevice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OnDeviceSummarizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;SummaryResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;SummaryResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OnDevice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;SummaryResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;preservedInput&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to notice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Failed&lt;/code&gt; carries the input.&lt;/strong&gt; The UI can always offer a retry or at minimum not silently discard what the user typed. The empty-toast bug existed because the error path returned &lt;code&gt;Unit&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The result is typed by source.&lt;/strong&gt; Your analytics should distinguish &lt;code&gt;Remote&lt;/code&gt; from &lt;code&gt;OnDevice&lt;/code&gt; — if 40% of your real-world requests land on-device, that is a product-quality signal, not just an engineering detail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For row 4 (backgrounding), do not deliver results via a callback that assumes an alive UI. Write the result to a &lt;code&gt;DataStore&lt;/code&gt; or database from a &lt;code&gt;WorkManager&lt;/code&gt; task or a coroutine scoped to the application, and let the UI observe it. Then "app backgrounded during request" stops being a special case at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring, not vibes
&lt;/h2&gt;

&lt;p&gt;For each matrix row, record: device, OS build, app version, network state before/after, time from request start to visible result, which path produced the result, and whether the input survived. On the Pixel 7 with a mid-range on-device model, my on-device fallback path consistently completed within a few seconds for short inputs — but I am deliberately not publishing a number here, because a single-device, single-model figure generalizes poorly and the method is the point, not my hardware's score. Run the matrix on the lowest-end device in your support list; that is where fallback latency decides whether the feature feels broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who should not do this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The free server is test infrastructure, not an SLA.&lt;/strong&gt; I used MonkeyCode's free tier because cost-free failure-injection is genuinely useful during development — you can fire row 6 (forced 5xx via a bad endpoint config) and row 1 timeouts all afternoon. I have no verified information about its quotas, rate limits, uptime guarantees, or how long the free tier lasts, so do not architect a production dependency on it. Point the same &lt;code&gt;RemoteSummarizer&lt;/code&gt; interface at your real backend before release and re-run the matrix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emulators lie about rows 3–5.&lt;/strong&gt; Cellular handoff and Doze behave differently on real hardware with carrier stacks and OEM battery managers (looking at you, every OEM with an aggressive app killer). Emulators are fine for rows 1, 2, and 6 only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This matrix assumes a remote-first design.&lt;/strong&gt; If your feature is privacy-sensitive enough that input must never leave the device, skip the fallback architecture entirely and go on-device-only — a fallback path is also a data-flow path you have to audit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Battery cost is untested here.&lt;/strong&gt; Six scenarios × many iterations of on-device inference is a real energy draw; I did not measure it in this round and you should before calling fallback "free."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd ask you to check
&lt;/h2&gt;

&lt;p&gt;If you have a remote-first AI feature in the wild, run row 4 tonight: send a request, background the app, kill the network, come back. Report what happened — device, OS, and whether the result recovered, restarted, or silently disappeared. My bet is that at least a third of apps quietly lose the input. If you want a cost-free endpoint to break against while you find out, MonkeyCode's free tier is where I ran mine; the matrix above works against any endpoint you control.&lt;/p&gt;

</description>
      <category>android</category>
      <category>mobile</category>
      <category>ai</category>
      <category>testing</category>
    </item>
    <item>
      <title>Benchmark Cloud LLM Calls Across Mobile Lifecycle Conditions Without Paying for the Backend</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Wed, 05 Aug 2026 06:38:51 +0000</pubDate>
      <link>https://dev.to/roronoa_/benchmark-cloud-llm-calls-across-mobile-lifecycle-conditions-without-paying-for-the-backend-1di0</link>
      <guid>https://dev.to/roronoa_/benchmark-cloud-llm-calls-across-mobile-lifecycle-conditions-without-paying-for-the-backend-1di0</guid>
      <description>&lt;p&gt;On-device inference gets most of the attention in mobile AI discussions, but plenty of shipped features still call a hosted model. The problem is that most "should we call a cloud model?" decisions are made from a laptop on office Wi-Fi, and the answer changes the moment your app is backgrounded mid-request, the user revokes network access via a data-saver toggle, or the device switches from Wi-Fi to a congested cell tower.&lt;/p&gt;

&lt;p&gt;This post is a reproducible test harness for measuring cloud LLM behavior under real mobile lifecycle conditions, plus a decision table for when a remote call is acceptable at all. I ran it against a free hosted endpoint, so the whole experiment costs nothing to repeat. Everything below is labeled as either measured on my device or a proposed step you should run on yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Device: Pixel 7 (Tensor G2), Android 14, battery at 62%, battery saver off unless noted&lt;/li&gt;
&lt;li&gt;App: minimal Kotlin client, OkHttp 4.12, coroutines, targetSdk 34&lt;/li&gt;
&lt;li&gt;Backend: a free hosted model endpoint (details below)&lt;/li&gt;
&lt;li&gt;Network: Wi-Fi, then forced LTE via airplane-mode toggle, then a throttled profile via Android emulator network settings for the 3G comparison&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The free backend problem
&lt;/h2&gt;

&lt;p&gt;Benchmarking cloud LLM calls usually means burning API credits just to learn that your timeout handling is wrong. For this experiment I used &lt;a href="https://monkeycode.ai" rel="noopener noreferrer"&gt;MonkeyCode&lt;/a&gt;, which currently offers free model access and a free server option, so I could stand up a throwaway endpoint and point the Android client at it without a billing account.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach. I have not verified quotas, model lineup, or how long the free tier stays free, so treat it as a zero-cost test backend, not infrastructure you should depend on. The harness below works against any OpenAI-compatible endpoint — swap the base URL and key and nothing else changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The harness: measure the lifecycle, not the happy path
&lt;/h2&gt;

&lt;p&gt;The interesting failures happen at lifecycle transitions, so the harness drives transitions explicitly. This is Kotlin for Android; the same structure ports to iOS with URLSession and background tasks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LifecycleBenchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OkHttpClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;ttftMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt;      &lt;span class="c1"&gt;// time to first token, null if failed&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;totalMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Outcome&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Outcome&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;COMPLETED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;CANCELLED_BY_SYSTEM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;STALE_ON_RESUME&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Sample&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;start&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SystemClock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elapsedRealtime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;ttft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;call&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buildRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;byteStream&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;bufferedReader&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;useLines&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                    &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ttft&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNotBlank&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                            &lt;span class="n"&gt;ttft&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SystemClock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elapsedRealtime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nc"&gt;Sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SystemClock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elapsedRealtime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Outcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;COMPLETED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SocketTimeoutException&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Outcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TIMEOUT&lt;/span&gt;
        &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Canceled"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Outcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CANCELLED_BY_SYSTEM&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Outcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;STALE_ON_RESUME&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;STALE_ON_RESUME&lt;/code&gt; case is the one most apps get wrong: the request was in flight, the app went to the background, the OS suspended or killed the socket, and on resume you hold a response object that will never complete. The harness logs it as its own outcome so you can count how often it actually happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test matrix
&lt;/h2&gt;

&lt;p&gt;Run each row at least 10 times. Record device, OS build, battery level, and network state alongside every sample.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Transition under test&lt;/th&gt;
&lt;th&gt;Expected observation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Foreground, Wi-Fi, screen on&lt;/td&gt;
&lt;td&gt;None (baseline)&lt;/td&gt;
&lt;td&gt;Stable TTFT distribution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Background mid-request&lt;/td&gt;
&lt;td&gt;Home button 2s after request starts&lt;/td&gt;
&lt;td&gt;Socket killed or response delayed until resume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Wi-Fi → LTE handoff&lt;/td&gt;
&lt;td&gt;Toggle airplane mode mid-stream&lt;/td&gt;
&lt;td&gt;Stream breaks; measure how OkHttp reports it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Throttled 3G profile&lt;/td&gt;
&lt;td&gt;Emulator network throttling&lt;/td&gt;
&lt;td&gt;TTFT inflates; find your real timeout floor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Battery saver on&lt;/td&gt;
&lt;td&gt;Enable before request&lt;/td&gt;
&lt;td&gt;Background network deferred; request may queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Permission-adjacent loss&lt;/td&gt;
&lt;td&gt;Revoke then restore network access via data-saver exemption&lt;/td&gt;
&lt;td&gt;App sees failure with no signal why&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Process death + restore&lt;/td&gt;
&lt;td&gt;"Don't keep activities" toggle mid-request&lt;/td&gt;
&lt;td&gt;Request gone; does your UI recover or hang?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Row 7 is the one that decides whether your feature is shippable. If the answer is "the spinner hangs forever," you don't have a latency problem, you have a state-recovery problem, and no amount of model tuning fixes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured vs. what you should verify
&lt;/h2&gt;

&lt;p&gt;Measured on the Pixel 7 above: baseline TTFT on Wi-Fi was consistent enough to be useful; the background-mid-request scenario produced &lt;code&gt;CANCELLED_BY_SYSTEM&lt;/code&gt; in most runs, which confirms that a streaming request cannot be trusted to survive backgrounding on stock Android 14 without a foreground service or &lt;code&gt;WorkManager&lt;/code&gt; handoff. The Wi-Fi → LTE handoff broke the stream every time — no silent recovery.&lt;/p&gt;

&lt;p&gt;I am deliberately not publishing the raw numbers here, because TTFT against a free shared endpoint reflects that endpoint's current load, not a property of your future production backend. The numbers that matter are &lt;em&gt;yours&lt;/em&gt;, on &lt;em&gt;your&lt;/em&gt; device class, against &lt;em&gt;your&lt;/em&gt; endpoint. If you want a zero-cost starting point for that, MonkeyCode's free model access and free server are enough to run this matrix today; for paid production planning, re-run the same matrix against your real provider before drawing conclusions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision table: is a cloud call acceptable for this feature?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature property&lt;/th&gt;
&lt;th&gt;Cloud call OK?&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Result needed within one foreground session&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Lifecycle risk is bounded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Must work offline or in airplane mode&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;On-device or queued-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streams for &amp;gt;10s and user may background app&lt;/td&gt;
&lt;td&gt;Risky&lt;/td&gt;
&lt;td&gt;Needs foreground service or resumable design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input is sensitive (health, children, journal)&lt;/td&gt;
&lt;td&gt;Prefer on-device&lt;/td&gt;
&lt;td&gt;Privacy and backup-exclusion concerns dominate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure must be silent and auto-recovered&lt;/td&gt;
&lt;td&gt;Only with WorkManager retry + idempotency key&lt;/td&gt;
&lt;td&gt;Otherwise duplicates or hangs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Limitations and who should not use this approach
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Free hosted endpoints have unknown rate limits and no latency SLA. Use them to validate your &lt;em&gt;handling&lt;/em&gt; of failure, not to estimate production p95.&lt;/li&gt;
&lt;li&gt;This harness tests one device and one OS. iOS background execution rules differ substantially; port before generalizing.&lt;/li&gt;
&lt;li&gt;I did not measure energy impact here. TTFT under battery saver (row 5) hints at it, but a real claim needs a power profiler run.&lt;/li&gt;
&lt;li&gt;If your feature requires guaranteed delivery, deterministic cost per request, or data residency, a free shared test backend is the wrong tool from day one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ask
&lt;/h2&gt;

&lt;p&gt;If you run this matrix, post your row 2 and row 7 outcomes with device, OS, and network state: did the in-flight request recover, restart, or silently disappear? That pair of rows tells you more about shippability than any benchmark average.&lt;/p&gt;

</description>
      <category>android</category>
      <category>mobile</category>
      <category>ai</category>
      <category>testing</category>
    </item>
    <item>
      <title>Measure Before You Choose: On-Device, Cloud API, or a Small Server for Your Mobile LLM Feature</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Tue, 04 Aug 2026 08:53:07 +0000</pubDate>
      <link>https://dev.to/roronoa_/measure-before-you-choose-on-device-cloud-api-or-a-small-server-for-your-mobile-llm-feature-4o1m</link>
      <guid>https://dev.to/roronoa_/measure-before-you-choose-on-device-cloud-api-or-a-small-server-for-your-mobile-llm-feature-4o1m</guid>
      <description>&lt;p&gt;A few weeks ago I sketched three deployment options for a summarization feature in a React Native app: run a small model on-device, call a hosted model API, or put a small model on a cheap server I control. Every option had a plausible-sounding argument. None of the arguments survived contact with the question that actually matters on mobile: &lt;em&gt;what does this cost in energy, latency, and failure modes on a real phone, on a real network?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This article is the measurement harness I built to answer that question honestly. It is a &lt;strong&gt;reproducible method&lt;/strong&gt;, not a benchmark report — I am deliberately not publishing numbers here, because numbers from my test device would be useless for your device class, your model, and your network. What transfers is the procedure, the instrumentation, and the decision table at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three placements, framed as mobile constraints
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Placement&lt;/th&gt;
&lt;th&gt;Latency driver&lt;/th&gt;
&lt;th&gt;Energy driver&lt;/th&gt;
&lt;th&gt;Failure mode to test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;On-device inference&lt;/td&gt;
&lt;td&gt;Model size vs. SoC/NPU&lt;/td&gt;
&lt;td&gt;Sustained CPU/GPU/NPU load, thermal throttle&lt;/td&gt;
&lt;td&gt;App backgrounded mid-inference, low power mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosted model API&lt;/td&gt;
&lt;td&gt;Network RTT + queueing&lt;/td&gt;
&lt;td&gt;Radio (cellular radio is expensive), TLS, retries&lt;/td&gt;
&lt;td&gt;Network switch (Wi-Fi → LTE), permission revoke&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small model on your own server&lt;/td&gt;
&lt;td&gt;Same as API + cold start&lt;/td&gt;
&lt;td&gt;Radio + keeping a server reachable&lt;/td&gt;
&lt;td&gt;Server sleep/restart, stale responses after reconnect&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third row is where free tiers matter for prototyping. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode currently offers free model access and a free server option, which makes it practical to stand up rows two and three without spending money during the measurement phase — a hosted model call for the API row, and a free server to host a small open model (or even just a mock endpoint) for the self-hosted row. Treat both as prototyping infrastructure, not a production commitment: verify current availability and limits yourself before designing around them, and do not assume any free tier is permanent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment design
&lt;/h2&gt;

&lt;p&gt;One device, one task, three placements, four lifecycle conditions. The task must be identical across placements — same prompt, same expected output class — so the only variable is &lt;em&gt;where&lt;/em&gt; execution happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixed conditions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Device: one physical phone (I used a mid-range Android; note your exact model, SoC, and OS build)&lt;/li&gt;
&lt;li&gt;Battery: start each run at 80%, unplugged, screen at fixed brightness&lt;/li&gt;
&lt;li&gt;Network: first pass on stable Wi-Fi, second pass on cellular with a Wi-Fi → LTE handover mid-run&lt;/li&gt;
&lt;li&gt;App state: foreground for baseline; then repeat with the app backgrounded for 10 s mid-task, and once more with low power mode enabled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The lifecycle matrix (kept small on purpose):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Foreground, stable Wi-Fi — your baseline&lt;/li&gt;
&lt;li&gt;Foreground, Wi-Fi → LTE handover during the request&lt;/li&gt;
&lt;li&gt;Backgrounded for 10 s mid-task, then resumed&lt;/li&gt;
&lt;li&gt;Low power mode enabled&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Four conditions is enough to expose the interesting differences. If a placement survives these, expand from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instrumentation: energy on Android
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;batterystats&lt;/code&gt; as a coarse but repeatable energy diff. Reset, run one placement N times, dump:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell dumpsys batterystats &lt;span class="nt"&gt;--reset&lt;/span&gt;
&lt;span class="c"&gt;# run the task 20 times from the app, same prompt each time&lt;/span&gt;
adb shell dumpsys batterystats &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; placement_ondevice.txt
adb bugreport bugreport_placement_ondevice.zip  &lt;span class="c"&gt;# for Battery Historian&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Record, per placement: estimated power use (mAh) attributed to your app's UID, mobile radio active time, and Wi-Fi radio active time. The radio-active-time row is where the on-device option usually wins, and where the two network options get separated by retry behavior. On iOS, use Xcode Instruments' Energy Log with the same fixed-brightness, fixed-charge protocol, and capture network traffic via the Network instrument.&lt;/p&gt;

&lt;p&gt;Caveat: &lt;code&gt;batterystats&lt;/code&gt; energy attribution is an estimate. It is good enough for &lt;em&gt;relative&lt;/em&gt; comparison across placements on the same device, not for publishing absolute numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instrumentation: latency that survives backgrounding
&lt;/h2&gt;

&lt;p&gt;Wall-clock timers lie when the app is backgrounded — the process may be suspended while your timer keeps conceptually running. Stamp events monotonically and persist them, so a killed process still leaves a trail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;TaskTrace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;placement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// "ondevice" | "api" | "server"&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;startedMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// SystemClock.elapsedRealtime()&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;networkSentMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;responseReceivedMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;finishedMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;interruptedBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// "backgrounded" | "network_switch" | "process_death"&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;         &lt;span class="c1"&gt;// "completed" | "restarted" | "lost"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// Persist each mutation immediately (Room/DataStore), not in memory.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log a trace row per attempt, and at the end of each condition compute per-placement: p50/p95 end-to-end latency, and — more important — the &lt;strong&gt;outcome distribution&lt;/strong&gt;: how many tasks completed, restarted, or silently disappeared. A placement that is fast but loses 30% of tasks when backgrounded loses to a slower one that always completes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to look for in the results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On-device row:&lt;/strong&gt; does latency degrade across 20 consecutive runs? That is thermal throttling, and it means your single-run demo was flattering. Check whether low power mode caps performance hard enough to break your UX.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API row:&lt;/strong&gt; compare radio active time against payload size. If you are sending large context, the upload cost on cellular can dominate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server row:&lt;/strong&gt; if your free server sleeps or restarts, the first request after idle will show it. That is fine for prototyping — but measure the cold-start penalty explicitly and decide whether a keep-alive ping is worth its own battery cost. (If you use the MonkeyCode free server here, this cold-start measurement is exactly the thing to run before building on it.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All rows, condition 2:&lt;/strong&gt; a mid-request Wi-Fi → LTE handover. Does your HTTP client retry transparently, or does the user see a dead spinner? Reproducible kill-switch testing helps here — run the API/server rows against an endpoint you control (this is where having your own free server is genuinely useful: you can kill it on demand).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal from your measurements&lt;/th&gt;
&lt;th&gt;Choose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;On-device p95 acceptable, no thermal collapse, task loss ≈ 0 when backgrounded&lt;/td&gt;
&lt;td&gt;On-device — best privacy, no radio cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-device throttles or exceeds latency budget; payload small; network mostly stable&lt;/td&gt;
&lt;td&gt;Hosted API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You need control over the model/endpoint, can tolerate cold starts, want a kill-switch for failure testing&lt;/td&gt;
&lt;td&gt;Your own small server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task loss &amp;gt; a few % in any network row and you cannot add client-side persistence + resume&lt;/td&gt;
&lt;td&gt;None of the network options until the resume path exists&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Limitations and who should skip this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Single-device results do not generalize across device classes. Repeat on at least one low-end device before committing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;batterystats&lt;/code&gt; attribution is approximate; treat it as relative signal.&lt;/li&gt;
&lt;li&gt;Free tiers (including MonkeyCode's free models and free server) are prototyping tools. Do not ship production traffic on assumptions about free-tier permanence, quotas, or SLAs I have not stated here because I cannot verify them for you.&lt;/li&gt;
&lt;li&gt;If your feature requires guaranteed offline behavior, the network rows are disqualifying by definition — skip straight to on-device.&lt;/li&gt;
&lt;li&gt;If you cannot persist task state client-side, no placement choice will save you from backgrounding losses; fix that first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run this harness, I would like to compare notes: what device and OS build, which lifecycle transition you used, and whether interrupted tasks recovered, restarted, or silently disappeared. That outcome distribution is the number I trust least from any single setup — including mine.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>ai</category>
      <category>android</category>
      <category>performance</category>
    </item>
    <item>
      <title>Benchmark an AI Coding Task Across Mobile Backgrounding, Network Switches, and Battery Pressure</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Fri, 31 Jul 2026 10:37:48 +0000</pubDate>
      <link>https://dev.to/roronoa_/benchmark-an-ai-coding-task-across-mobile-backgrounding-network-switches-and-battery-pressure-39e5</link>
      <guid>https://dev.to/roronoa_/benchmark-an-ai-coding-task-across-mobile-backgrounding-network-switches-and-battery-pressure-39e5</guid>
      <description>&lt;p&gt;Most AI coding assistants are demoed on a desk: laptop plugged in, stable Wi-Fi, full attention. But the moment I approve or monitor a long-running AI task from a phone, the environment becomes hostile — the app gets backgrounded, the device hops from Wi-Fi to LTE, iOS starts throttling background execution, and the battery drops below 20%.&lt;/p&gt;

&lt;p&gt;This article is a reproducible benchmark method for one question: &lt;strong&gt;does an AI coding task survive the mobile lifecycle, and can you tell the difference between 'recovered', 'restarted', and 'silently dropped'?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Test environment (record yours)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Device: iPhone 14, iOS 17.x (note your exact build)&lt;/li&gt;
&lt;li&gt;App state: task started from the foreground, screen unlocked&lt;/li&gt;
&lt;li&gt;Network: controlled Wi-Fi with the option to toggle airplane mode, plus LTE fallback&lt;/li&gt;
&lt;li&gt;Power: start at ~60% battery, charger off; repeat one run in Low Power Mode&lt;/li&gt;
&lt;li&gt;Backend: a remote AI coding agent endpoint. For these runs I used MonkeyCode, which offers free model access and a free server option — convenient for this kind of throwaway benchmark because I didn't have to provision my own GPU box or burn a paid quota on test traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Any agent backend with a task ID you can poll works for this method; the benchmark is about the lifecycle, not the vendor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one metric that matters
&lt;/h2&gt;

&lt;p&gt;For each run, classify the final state:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Recovered&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Same task ID, output continues from where the connection dropped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Restarted&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;New task ID or duplicate execution; original work discarded or redone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Silent loss&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;UI shows 'running' or a spinner, but the backend has no live task&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Silent loss is the dangerous one. It looks like progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark script (client side)
&lt;/h2&gt;

&lt;p&gt;The client just needs to launch a task, poll by ID, and log transitions with timestamps. Pseudocode you can adapt to React Native, Flutter, or a plain fetch loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lifecycle-bench.mjs — run in your app's debug console or a Node harness&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;extra&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;extra&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;launchTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BACKEND&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/tasks`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;taskId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;stamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;launched&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;taskId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(;;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BACKEND&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/tasks/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;lastStatus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;stamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="nx"&gt;lastStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;stamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;poll_error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// On app foregrounding, run: poll(savedTaskId)&lt;/span&gt;
&lt;span class="c1"&gt;// Then diff the log against the backend's task history.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After each run, compare the client log with server-side task history. If the client saw &lt;code&gt;poll_error&lt;/code&gt; for 4 minutes and the server shows the task still executing, that's a &lt;em&gt;recovered&lt;/em&gt; run. If the server shows the task was killed at the disconnect, your UI lied — classify accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transition matrix
&lt;/h2&gt;

&lt;p&gt;Run the same prompt (a medium-length task, e.g. 'refactor this module and run the tests') under each condition. One variable per run:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Baseline&lt;/strong&gt; — foreground, Wi-Fi, charger on. Establishes expected duration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background at T+30s&lt;/strong&gt; — press home, wait 5 minutes, return. Does polling resume? Does the task still exist?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wi-Fi → LTE handoff mid-task&lt;/strong&gt; — toggle Wi-Fi off while a poll is in flight. Watch for duplicated requests on reconnect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Airplane mode 3 minutes&lt;/strong&gt; — full offline window. On restore, does the client re-attach to the task ID, or submit a new one?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low Power Mode + background&lt;/strong&gt; — iOS aggressively defers network activity. Expect longer gaps; check whether the agent times out server-side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App kill (swipe away)&lt;/strong&gt; — the harsh case. On relaunch, can you recover the task ID from local storage and re-attach?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Expected observations from my runs: conditions 2–4 are survivable when the client persists the task ID and the backend treats tasks as resumable by ID. Condition 5 is where I've seen silent loss most often — the backend's idle timeout fires while iOS defers the client's keepalive. Condition 6 fails entirely if the task ID only lives in memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to fix when a run fails
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Restarted instead of recovered:&lt;/strong&gt; persist &lt;code&gt;taskId&lt;/code&gt; (Keychain/Keystore, not just AsyncStorage if it's sensitive) and re-attach on launch before offering a 'start new task' button.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent loss:&lt;/strong&gt; add a heartbeat with a server-visible &lt;code&gt;lastSeenAt&lt;/code&gt;. If server &lt;code&gt;lastSeenAt&lt;/code&gt; is stale but UI shows 'running', surface 'connection lost — task status unknown' instead of a spinner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate execution after reconnect:&lt;/strong&gt; make task creation idempotent (client-generated idempotency key), so a retried POST can't fork the run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Single device, single OS version. Android's background execution model (Doze, OEM battery killers) behaves differently — repeat the matrix there before generalizing.&lt;/li&gt;
&lt;li&gt;I tested task &lt;em&gt;continuity&lt;/em&gt;, not output quality or model latency; those need their own benchmarks with device data.&lt;/li&gt;
&lt;li&gt;Free tiers change. The free model access and free server I used here are availability facts as of this writing, not a guarantee of quotas, duration, or performance — verify before building a workflow around them.&lt;/li&gt;
&lt;li&gt;Results depend heavily on backend timeout policy; your agent service may behave differently under the same client conditions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who shouldn't use this approach
&lt;/h2&gt;

&lt;p&gt;If your tasks are sub-second, this whole matrix is overkill — just retry. And if the task handles sensitive code or credentials on a device you don't control, a free hosted server is the wrong target entirely; test continuity against infrastructure you're authorized to use.&lt;/p&gt;

&lt;p&gt;If you run this matrix on your own setup, I'd like to compare notes: device, OS version, which transition you triggered, and whether the task recovered, restarted, or silently disappeared. That last one is the number nobody's dashboard shows.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>ios</category>
      <category>ai</category>
      <category>testing</category>
    </item>
    <item>
      <title>Benchmark Mobile AI Reconnect Recovery Across Lifecycle Transitions With a Server You Can Kill</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Fri, 31 Jul 2026 09:04:31 +0000</pubDate>
      <link>https://dev.to/roronoa_/benchmark-mobile-ai-reconnect-recovery-across-lifecycle-transitions-with-a-server-you-can-kill-2836</link>
      <guid>https://dev.to/roronoa_/benchmark-mobile-ai-reconnect-recovery-across-lifecycle-transitions-with-a-server-you-can-kill-2836</guid>
      <description>&lt;p&gt;Last week I was testing an AI chat feature on my iPhone 14 (iOS 17.5, React Native 0.74, Hermes). The flow looked fine on Wi-Fi. Then I walked into an elevator mid-stream, the radio dropped, iOS suspended the app, and when I came back the conversation had silently lost the last two turns. No error. No retry. Just a gap in the transcript that only the user would notice.&lt;/p&gt;

&lt;p&gt;The problem with testing this class of bug is that I don't control the model backend. I can't tell a hosted LLM API to hang for 40 seconds, drop a socket mid-response, or return a truncated chunk — and those are exactly the conditions my app fails under. So I started routing the app through a relay server I &lt;em&gt;can&lt;/em&gt; kill, and suddenly reconnect behavior became a benchmarkable property instead of a vibe.&lt;/p&gt;

&lt;p&gt;This post is the harness, the test matrix, and how I run it. Everything below is a reproducible setup, not a finished benchmark report — where I expect a result rather than having measured one, I say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a relay you control
&lt;/h2&gt;

&lt;p&gt;Point your mobile app at a thin relay in front of the real model API. The relay proxies requests but lets you inject:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt; — delay upstream responses by N ms to simulate a congested cell handoff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard drops&lt;/strong&gt; — close the socket mid-stream to simulate the elevator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suspension windows&lt;/strong&gt; — pause the upstream so iOS has time to background your app before bytes arrive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your AI task survives a relay that is &lt;em&gt;deliberately hostile&lt;/em&gt;, it has a chance of surviving real networks.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach. The reason it fits here is practical: MonkeyCode offers free model access and a free server option, which means the relay plus a model backend can run without me paying for a VPS or burning API budget during long fault-injection sessions. Any equivalent free tier works for this harness — the point is that cost stops being an excuse to skip the test, not that one provider is uniquely fast or reliable (I have not benchmarked MonkeyCode's latency and make no claim about it).&lt;/p&gt;

&lt;h2&gt;
  
  
  The fault-injection relay
&lt;/h2&gt;

&lt;p&gt;Minimal Node 20 relay. It streams from an upstream OpenAI-compatible endpoint and applies a per-request fault profile from headers, so the test runner — not the app — controls the chaos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// relay.mjs — run: node relay.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UPSTREAM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UPSTREAM_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// your model endpoint&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UPSTREAM_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-fault-delay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dropAfterBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-fault-drop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;up&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;UPSTREAM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;up&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;up&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;up&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;sent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;sent&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dropAfterBytes&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;sent&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;dropAfterBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// simulate a radio drop mid-stream&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8787&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;relay on :8787&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point the app's base URL at &lt;code&gt;http://&amp;lt;your-lan-ip&amp;gt;:8787&lt;/code&gt;, and drive fault profiles from the test harness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clean run&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8787/v1/chat &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{...}'&lt;/span&gt;

&lt;span class="c"&gt;# 8s upstream hang — long enough to background the app and come back&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8787/v1/chat &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'x-fault-delay: 8000'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{...}'&lt;/span&gt;

&lt;span class="c"&gt;# Drop mid-stream after 2KB&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8787/v1/chat &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'x-fault-drop: 2048'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{...}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The test matrix
&lt;/h2&gt;

&lt;p&gt;This is the part most teams skip. Each cell is a distinct failure mode, and "works on Wi-Fi" only covers the first row. My matrix for an AI task with an in-flight request:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Lifecycle transition&lt;/th&gt;
&lt;th&gt;Fault injected&lt;/th&gt;
&lt;th&gt;Expected behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;None, foreground&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;stream completes, transcript persisted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Background 5s, return&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;stream resumes or cleanly resumes from checkpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Background 60s (iOS suspends)&lt;/td&gt;
&lt;td&gt;delay 8s&lt;/td&gt;
&lt;td&gt;task marked interrupted; on return, partial output shown + explicit resume prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Network drop mid-stream&lt;/td&gt;
&lt;td&gt;drop 2KB&lt;/td&gt;
&lt;td&gt;partial tokens preserved, retry offered, no duplicate user turn on retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Airplane mode on/off&lt;/td&gt;
&lt;td&gt;drop + offline&lt;/td&gt;
&lt;td&gt;task queued locally, replays exactly once on reconnect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;App killed by swipe&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;cold start restores draft + interrupted task state from disk, not memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Low Power Mode + background&lt;/td&gt;
&lt;td&gt;delay 8s&lt;/td&gt;
&lt;td&gt;same as #3; no silent assumption that background fetch will run&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For each cell I record: device, iOS version, RN version, network (Wi-Fi/LTE/airplane), battery state, fault profile, and one of three outcomes — &lt;strong&gt;recovered&lt;/strong&gt;, &lt;strong&gt;restarted&lt;/strong&gt;, or &lt;strong&gt;silently lost&lt;/strong&gt;. The third outcome is the one that matters; it's the bug my users found in the elevator.&lt;/p&gt;

&lt;p&gt;The single most common root cause I've hit across these tests: task state living only in memory (a Redux store or component state) instead of being checkpointed to disk after every streamed chunk. &lt;code&gt;AppState&lt;/code&gt; listeners and NetInfo alone cannot save you if there is nothing durable to restore.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this harness does not tell you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It reproduces &lt;em&gt;network&lt;/em&gt; and &lt;em&gt;lifecycle&lt;/em&gt; faults deterministically, but not OS-level variance. iOS background execution budgets differ by device and usage history; run the matrix on the oldest device you support, not your daily driver.&lt;/li&gt;
&lt;li&gt;The relay adds a hop. Don't use round-trip times through it as production latency numbers.&lt;/li&gt;
&lt;li&gt;"Free tier" availability, quotas, and model lineups change. Verify what's actually offered before building a CI job on top of it, and have a fallback endpoint.&lt;/li&gt;
&lt;li&gt;I have not yet run this matrix on Android; the backgrounding semantics (Doze, OEM task killers) are different enough that results won't transfer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who should not use this approach
&lt;/h2&gt;

&lt;p&gt;If your AI feature is a single fire-and-forget request with no streaming and no retry semantics, this is overkill — a unit test with a mocked 500 is enough. And if your threat model is about the &lt;em&gt;provider's&lt;/em&gt; uptime rather than the device's radio, inject faults at the client layer (e.g., a URLProtocol/OkHttp interceptor) instead of standing up a relay.&lt;/p&gt;

&lt;p&gt;If you try the matrix, I'd genuinely like the comparable data: device, OS version, which transition you hit, and whether the task recovered, restarted, or silently disappeared. That third column is where the interesting bugs live.&lt;/p&gt;

&lt;p&gt;If you want to try the relay setup without provisioning anything, MonkeyCode's free model access and free server option are one way to get both halves running — but the harness above works against any endpoint you control.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>ios</category>
      <category>ai</category>
      <category>testing</category>
    </item>
    <item>
      <title>"Test Mobile CI Repair Across Backgrounding and Reconnect"</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Wed, 29 Jul 2026 09:47:40 +0000</pubDate>
      <link>https://dev.to/roronoa_/test-mobile-ci-repair-across-backgrounding-and-reconnect-1bca</link>
      <guid>https://dev.to/roronoa_/test-mobile-ci-repair-across-backgrounding-and-reconnect-1bca</guid>
      <description>&lt;p&gt;Start a repair on Wi-Fi, background the app, let the operating system suspend it, and reconnect on cellular. The dangerous result is not a visible error; it is two repairs that both believe they own the same failing check.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the announcement establishes
&lt;/h2&gt;

&lt;p&gt;GitHub announced on July 23, 2026 that GitHub Mobile can use Copilot cloud agent to fix failing Actions checks. The source describes the product path; the matrix below is a proposed test protocol, not measured product behavior. &lt;a href="https://github.blog/changelog/2026-07-23-github-mobile-fix-failing-actions-checks-with-copilot-cloud-agent/" rel="noopener noreferrer"&gt;Read the primary source&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For test track 10.1, the engineering claim here is narrower than the announcement: the surrounding workflow needs a contract that remains valid when metadata, transport, people, or executors change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The artifact
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Device state&lt;/th&gt;
&lt;th&gt;Network transition&lt;/th&gt;
&lt;th&gt;Expected observation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;foreground&lt;/td&gt;
&lt;td&gt;Wi-Fi → cellular&lt;/td&gt;
&lt;td&gt;same operation ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;background&lt;/td&gt;
&lt;td&gt;online → offline&lt;/td&gt;
&lt;td&gt;local state becomes uncertain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;terminated&lt;/td&gt;
&lt;td&gt;offline → online&lt;/td&gt;
&lt;td&gt;server state reconciled first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;foreground&lt;/td&gt;
&lt;td&gt;account switched&lt;/td&gt;
&lt;td&gt;prior operation inaccessible&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use an oracle tuple: &lt;code&gt;(repository, workflow_run_id, failing_check_sha, operation_id, latest_server_state)&lt;/code&gt;. The UI may render success only when the returned commit descends from the tested SHA and the server operation is terminal.&lt;/p&gt;

&lt;p&gt;For test track 10.2, this is a design fixture, not executed code. Pin language and dependency versions before turning it into a repository test, and replace example identities and timestamps with disposable values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Success path
&lt;/h2&gt;

&lt;p&gt;After reconnect, the app fetches by operation ID, shows one repair branch, and links the new check run. Repeated taps return the existing operation instead of starting another.&lt;/p&gt;

&lt;p&gt;For test track 10.3, a successful demonstration records inputs, policy or schema version, decision, and final identifier. It does not infer correctness from a confidence label, status badge, or fluent output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure path and regression plan
&lt;/h2&gt;

&lt;p&gt;If the check SHA changed, label the result stale and require a new review. If local and server states disagree, prefer server evidence while preserving a visible reconciliation warning.&lt;/p&gt;

&lt;p&gt;Record device model, OS and app version, battery mode, permission state, foreground transition, network type, timestamps, operation IDs, commits, and whether recovery resumed, restarted, or failed.&lt;/p&gt;

&lt;p&gt;For test track 10.4, the acceptance gate is binary: the negative fixture must produce no unauthorized or duplicate side effect, while the positive fixture must remain traceable to its initial evidence. Expected output should be documented before execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup and rollback
&lt;/h2&gt;

&lt;p&gt;Cancel only nonterminal work, close test branches after preserving evidence, and revert the repair through normal repository review. Remove test notifications and sign out of disposable accounts.&lt;/p&gt;

&lt;p&gt;For test track 10.5, cleanup must preserve enough sanitized evidence to distinguish cancellation, rejection, stale work, and successful completion. Never solve recovery by silently marking an uncertain operation successful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;For test track 10.6, this article proposes a compact engineering exercise and reports no execution results. It does not evaluate service availability, security, accessibility conformance, productivity, or comparative quality. Product previews can change, and a local fixture cannot reproduce every hosted-system failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical development environment
&lt;/h2&gt;

&lt;p&gt;For cloud-side development experiments, MonkeyCode is an open-source AGPL-3.0 AI development platform with an overseas hosted option. It includes a managed server-side cloud development environment, integrated models, task and requirement management, and build, test, and preview workflows. It is free to start. These statements do not mean the GitHub or OpenAI capability discussed above exists in MonkeyCode. Check the console for current quotas, models, regions, duration, and pricing before planning work. &lt;a href="https://ly.cyberserval.tech/iIETXiF" rel="noopener noreferrer"&gt;Open the campaign workspace&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Disclosure: This article promotes MonkeyCode using an official campaign link. I’m a MonkeyCode user, not affiliated with the project, and I receive no commission from this link.&lt;/p&gt;

&lt;p&gt;AI assistance disclosure: This article was drafted with AI assistance and reviewed against the cited primary sources.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>github</category>
      <category>testing</category>
      <category>ci</category>
    </item>
    <item>
      <title>"Approve a Preview on Mobile Without Trusting a Stale Network Handoff"</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Tue, 28 Jul 2026 03:41:46 +0000</pubDate>
      <link>https://dev.to/roronoa_/approve-a-preview-on-mobile-without-trusting-a-stale-network-handoff-595i</link>
      <guid>https://dev.to/roronoa_/approve-a-preview-on-mobile-without-trusting-a-stale-network-handoff-595i</guid>
      <description>&lt;p&gt;A phone opens a preview on Wi-Fi. The reviewer walks outside, the browser reconnects on cellular, and the old page still looks approvable. Network continuity is not evidence continuity. Bind the action to a short-lived preview token containing the exact artifact identity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"preview_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prv_01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bld_01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"commit_sha"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"replace-me"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evidence_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issued_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"replace-with-UTC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"replace-with-UTC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nonce"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"single-use-replace-me"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server—not the phone clock—accepts approval only if the token is unexpired, unused, authorized for the reviewer, and still matches current preview, build, commit, and evidence version. Any rebuild, changed test result, superseding preview, logout, or explicit revocation makes it stale. The client can hide a stale button, but server rejection is the boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Device and network grid
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Device/browser&lt;/th&gt;
&lt;th&gt;Start&lt;/th&gt;
&lt;th&gt;Handoff&lt;/th&gt;
&lt;th&gt;Interruption&lt;/th&gt;
&lt;th&gt;Required result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;small phone / A&lt;/td&gt;
&lt;td&gt;Wi-Fi&lt;/td&gt;
&lt;td&gt;cellular&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;revalidate before approve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;small phone / A&lt;/td&gt;
&lt;td&gt;cellular&lt;/td&gt;
&lt;td&gt;offline&lt;/td&gt;
&lt;td&gt;background&lt;/td&gt;
&lt;td&gt;no optimistic success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;large phone / B&lt;/td&gt;
&lt;td&gt;Wi-Fi&lt;/td&gt;
&lt;td&gt;captive portal&lt;/td&gt;
&lt;td&gt;reload&lt;/td&gt;
&lt;td&gt;explicit blocked state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tablet / B&lt;/td&gt;
&lt;td&gt;Wi-Fi&lt;/td&gt;
&lt;td&gt;cellular&lt;/td&gt;
&lt;td&gt;build replaced&lt;/td&gt;
&lt;td&gt;old token rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;phone / A&lt;/td&gt;
&lt;td&gt;cellular&lt;/td&gt;
&lt;td&gt;Wi-Fi&lt;/td&gt;
&lt;td&gt;double tap&lt;/td&gt;
&lt;td&gt;one approval operation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Record exact OS/browser versions, power mode, preview/build IDs, network timestamps, token evidence version, HTTP outcome, and final server state. Do not record personal browsing data or raw bearer tokens.&lt;/p&gt;

&lt;p&gt;Unexecuted test sequence: open the identified preview; inspect its visible commit marker; background the browser; trigger a harmless rebuild from the fixture; switch networks; foreground; attempt the old action. Expected failure is an explicit stale response with no approval effect. Fetch the new preview and token, verify identifiers, approve once, then immediately switch networks again. Expected success is one durable operation ID whose artifact identity matches the screen.&lt;/p&gt;

&lt;p&gt;Stop release for approval during offline state, replay after token expiry, disagreement across devices, a changed artifact retaining the old evidence version, or duplicate effects from repeated taps. Cleanup means revoke unused tokens, delete preview and disposable workspace, remove test identities, and verify the action cannot be replayed from browser history. Preserve only sanitized IDs and outcomes.&lt;/p&gt;

&lt;p&gt;This grid cannot cover every radio, browser cache, service worker, VPN, captive portal, or clock condition. A token reduces stale approval risk but does not prove preview content integrity or reviewer intent. The protocol and expected results are proposed; no mobile test result is claimed. Record whether a stale page remains visible after rejection, because safe server behavior does not excuse a misleading screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect the grid to a temporary preview
&lt;/h2&gt;

&lt;p&gt;The overseas hosted MonkeyCode option is described by current official material as “Free to start,” with integrated models and managed server-side cloud development environments supporting build, test, and preview. It offers a surface for this handoff test; it does not make every model or cloud server free. Check the current console for availability, quotas, permitted regions, duration, possible future pricing, and applicable terms.&lt;/p&gt;

&lt;p&gt;Use this official campaign route after preparing the disposable device fixture: &lt;a href="https://ly.cyberserval.tech/iIETXiF" rel="noopener noreferrer"&gt;https://ly.cyberserval.tech/iIETXiF&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The mobile exit sequence revokes tokens, clears sanitized fixture data, deletes preview and workspace, and retries the old action only to confirm rejection. Any surviving authority or mismatched artifact identity fails the exercise.&lt;/p&gt;

&lt;p&gt;Disclosure: This article promotes MonkeyCode using an official campaign link. I’m a MonkeyCode user, not affiliated with the project, and I receive no commission from this link.&lt;/p&gt;

&lt;p&gt;AI assistance disclosure: This article was drafted with AI assistance and reviewed against the cited project materials.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>testing</category>
      <category>ux</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
