If your mobile CI is green, every test passes on the simulator, but App Store reviews keep piling up with "the app freezes" complaints, the problem isn't your tests — it's the hardware. Simulators and emulators mimic CPU/GPU behavior using the host machine's power; the real device's thermal throttling, low RAM, and weak antenna fall outside that mimicry. This post gives a concrete framework for which devices to buy: deriving a matrix from your own user data, the logic for picking a low-end device, your-own-farm-vs-cloud, and the time budget for real-device runs in CI.
💡 Pro Tip: Build your device matrix based not on "what I already have" but on the device+OS distribution in your own user analytics from Firebase/Play Console — otherwise you'll end up never testing the segment that complains the most.
Table of Contents
- The five bug classes simulators can't catch
- Deriving your device matrix from your user data
- Use your own analytics as the source
- Picking the low-end device: why it's the most important device
- Your own device farm vs. a device cloud: which for which job
- Real-device runs in CI and the time budget
- Pre-release manual smoke test checklist
- The annual device-farm refresh rule
- FAQ
- Which devices should I buy for testing?
- Which bugs does a simulator miss?
- Device cloud or my own devices?
- Which Android versions does Gradle Managed Devices support?
- How long can a real/managed device test take in CI?
- Update (September 2026)
- Conclusion
- Sources
The five bug classes simulators can't catch
Simulator/emulator tests catch logic bugs perfectly, but five bug classes systematically slip through:
- Thermal throttling: A desktop's cooling doesn't throttle the way a device's small chassis does; the slowdown from CPU frequency dropping during a 10-minute camera/AR session never happens in a simulator.
- Background eviction under low RAM: The OS can kill your app under memory pressure and reset its state; this behavior depends on the device's physical RAM, not the simulator's host-machine RAM.
- Weak/intermittent network + real antenna: The simulator routes the network layer through the host machine's connection; the device's own Wi-Fi/cellular antenna behaviors — packet loss on weak signal, reconnection delay — aren't simulated.
- Old GPU driver/rendering quirks: Driver incompatibilities on real GPUs for Metal/OpenGL/Vulkan (especially on three-to-four-year-old mid-range devices) don't show up on a simulator's desktop GPU.
- Hardware-rendered screenshot tests: As Google documents, the Automated Test Device (ATD) doesn't support screenshot tests that require hardware-accelerated rendering — this test class needs a real or "managed device," ATD isn't sufficient.
Deriving your device matrix from your user data
Device selection should start from real distribution, not personal preference. There are two axes: OS version and device segment.
On the OS-version side, the market is still fragmented: neither Android's nor iOS's user base sits on a single version — several major versions carry meaningful share at once, and even on iOS "everyone is on the latest version" doesn't hold. Since these percentages age fast, instead of a dated table here, derive the current distribution from your own analytics using the three-step method below.
Use your own analytics as the source
Play Console's "Devices" tab, Firebase Analytics' device model/OS dimension, App Store Connect's "Devices" report — these are the real distribution of your own user base. When building the matrix, in order:
- Sort device model + OS version pairs by session count over the last 90 days.
- Extract the smallest device+OS list that brings cumulative coverage to 80% (the long tail is usually individually low-share but collectively significant).
- Always manually add the "low-end device" described in the next section to this list — even if it barely shows up in analytics.
An example 6-device starter matrix looks like this (replace it with your own analytics):
Segment | Example device | OS target | Why it's in the matrix |
|---|---|---|---|
Flagship | Current Pixel/iPhone | Latest major version | Latest API behavior |
Upper-mid | 2-year-old flagship | Previous major version | Largest user slice |
Low-end | Budget segment, low RAM | Older supported version | Subject of the next section |
Old iOS | iPhone near end of support | Previous major iOS | Bottom edge of the support window |
Tablet | Large-screen Android/iPad | Current | Different layout/orientation |
Foldable | Fold/Flip class | Current | State loss on screen-size change |
Picking the low-end device: why it's the most important device
One of the six devices catches far more bugs than the rest: the one with the lowest RAM and weakest CPU. The reason is simple — upper-segment devices paper over your code bugs with raw hardware power; the low-end device can't. Background eviction, GC pauses, animation frame drops, cold-start time — all show up magnified there.
Practical rule: don't drop the low-end device as "the lowest-share segment" — deliberately keep it, for the opposite reason. In my experience, a release that runs well there also runs fine on every mid-tier segment; the reverse isn't true — a release tested only on the flagship may never even launch on the low end.
Picking the low-end device isn't just about one number (say, RAM alone); weigh three dimensions together: physical RAM (background-eviction behavior), CPU core count/frequency (cold-start time and animation frame duration), and storage speed (first-install + first-launch experience). Whichever device is weakest on all three while still holding a meaningful share in your analytics — that's your low-end device. Reducing this to one dimension can, say, lead you to pick a low-RAM but strong-CPU device and completely miss storage-driven first-launch problems.
Your own device farm vs. a device cloud: which for which job
Pricing models vary by provider and run duration; here's a decision framework instead of numbers:
- Your own farm: Fixed hardware cost + storage/charging/update maintenance overhead, but no "device busy" wait in the CI queue, and you can set up a fully isolated network environment (important for banking/payment testing).
- Device cloud (Firebase Test Lab-like): In Firebase's own words, a "cloud-based app-testing infrastructure" — gives access to a broad catalog of Android/iOS devices + fast Arm-hosted virtual devices, and integrates with CI systems. When a new device generation ships, maintenance isn't on you.
- Critical limit: Firebase Test Lab's own documentation explicitly says it's "not designed for backend load testing" — so think of cloud device testing as functional/UI testing, not performance/load testing.
I generally prefer using both together: I run the most frequently-run smoke/regression tests across a broad matrix in the cloud, and do deep profiling (Instruments/Android Studio Profiler) of the low-end device on the physical unit I keep on hand.
A qualitative comparison for the decision:
Criterion | Your own farm | Device cloud |
|---|---|---|
CI queue wait | None (device is on hand) | Depends on provider capacity |
Network isolation | Full control is yours | Depends on provider's network environment |
New device generation upkeep | You buy and maintain it | Provider keeps it current |
Load/performance testing | Possible if suitable | Firebase Test Lab explicitly excludes it in its own docs |
Screenshot testing | Full support on a real device | ATD doesn't support tests requiring hardware rendering |
Real-device runs in CI and the time budget
On the Android side, Gradle Managed Devices automatically spins up the device profiles you define in build.gradle and runs your tests in CI. Per Google's documentation, this feature requires device profiles at API level 27 and above; the Automated Test Device (ATD) profile only supports API 30 and can't run screenshot tests that require hardware-accelerated rendering.
1// build.gradle.kts — managed device profile example2android {3 testOptions {4 managedDevices {5 localDevices {6 create("lowEndDevice") {7 device = "Pixel 6a"8 apiLevel = 309 systemImageSource = "aosp-atd"10 }11 }12 }13 }14}The time budget comes straight from the source — these settings apply in the firebaseTestLab { testOptions { ... } } block defined for Firebase Test Lab devices: the default for timeoutMinutes is 15 minutes; it can be set up to a maximum of 45 minutes on physical devices and 60 minutes on virtual devices. The default for retries (maxTestReruns) is 0, with a maximum of 10. For smart sharding, targetedShardDurationMinutes should be kept at least 5 minutes below timeoutMinutes — otherwise a shard may fail to finish before hitting the timeout.
On the Firebase Test Lab side, the matrix is built from device + OS version + locale + orientation combinations and can be integrated into CI systems:
1# Firebase Test Lab — CI run with device+OS+locale+orientation matrix2gcloud firebase test android run \3 --type instrumentation \4 --app app-debug.apk \5 --test app-debug-androidTest.apk \6 --device model=low-end-device,version=30,locale=tr_TR,orientation=portrait \7 --device model=flagship-device,version=34,locale=en_US,orientation=portrait \8 --timeout 20m(The model= values here are examples; get real model IDs from the gcloud firebase test android models list output.)
On the iOS side, the xcodebuild test command (or Product > Test in Xcode) kicks off the run; per Apple's documentation, this command produces an Xcode Test Results (.xcresults) bundle containing session results, coverage (if enabled), and other logs; you can read the top-level summary from the Report Navigator. Xcode Cloud, beyond that, automates scheduled, broader-scope test runs:
1xcodebuild test \2 -scheme "AppUITests" \3 -destination "platform=iOS,name=Low End Device" \4 -resultBundlePath ./ci-result.xcresultPre-release manual smoke test checklist
No matter how broad the automated matrix is, there needs to be a list of things to manually check on the low-end device right before shipping — this is where you catch flaky-automation gaps or the visual/feel issues CI misses:
- Cold-start time: fully quit the app and time the first launch on the low-end device with a stopwatch.
- Background and back again: open a few heavy apps, then return to your app and check for state loss.
- Weak-signal simulation: put the device in airplane mode and reconnect, watch the reconnection behavior.
- Keyboard + rotation: with the keyboard open on a form screen, rotate the device landscape/portrait, look for layout breakage.
- Notification + deep link: tap a push notification to open the app from cold.
- Low battery mode: turn on the device's battery saver and check background task (sync, notification) behavior.
Instead of tracking this list by hand, putting it in a checklist file makes the job easier:
1# smoke-checklist.sh — pre-release low-end device check2echo "1) Cold-start time (sec):"3echo "2) State loss on returning from background: [ ]"4echo "3) Weak-signal reconnection: [ ]"5echo "4) Keyboard + rotation layout: [ ]"6echo "5) Push -> deep link cold launch: [ ]"7echo "6) Low battery mode background task: [ ]"The annual device-farm refresh rule
A practical approach: instead of replacing the device farm wholesale, recalculate the matrix once a year against user analytics, and only remove devices whose analytics share has shrunk to insignificance, replacing them with a device from the current low-end segment. You don't need to refresh the flagship device every year — the upper segment already papers over code bugs; what actually needs to change is the low-end and mid-tier representative.
Tie the refresh schedule to the analytics calendar, not the release calendar: when platforms announce their annual major releases (a new Android major version, a new iOS major version), don't reflexively swap devices — first look at how fast the new version is actually spreading in its own user base. As you saw in the section above, even on iOS multiple major versions can carry meaningful share at the same time; so a "new version shipped, retire the old device" decision made without data risks leaving your low-end user base untested.
GOLDEN TIP
The most valuable insight in this article
This tip holds the article's most important takeaway.
Easter Egg
You found a hidden gem!
There's a hidden detail in this section. Want to uncover it?
Reader Reward
I put together a checklist that gathers all the decisions in this post onto a single page; you can apply it in order before a release or when setting up a new device farm.
FAQ
Which devices should I buy for testing?
Instead of a random budget/flagship pick, sort device+OS pairs by session count over the last 90 days from your own user analytics (Play Console/Firebase/App Store Connect device reports) and extract the smallest list that brings cumulative share to 80%; add the lowest-RAM active segment to that as the low-end device by hand.
Which bugs does a simulator miss?
Thermal throttling, background eviction under low RAM, weak/intermittent network + real antenna behavior, old GPU driver rendering quirks, and screenshot tests requiring hardware-accelerated rendering — these five classes all fall outside the host-machine resources the simulator emulates.
Device cloud or my own devices?
They solve different problems: a device cloud (like Firebase Test Lab) scales functional testing across a broad device/OS/locale/orientation matrix, but its own documentation states it's "not designed for backend load testing"; your own farm is better suited for scenarios requiring an isolated network environment and deep profiling kept on hand.
Which Android versions does Gradle Managed Devices support?
Per Google's documentation, managed device profiles require API level 27 and above; the Automated Test Device (ATD) profile only supports API 30 and can't run screenshot tests that require hardware-accelerated rendering.
How long can a real/managed device test take in CI?
On Firebase Test Lab devices, the default for timeoutMinutes is 15 minutes; it can be raised up to a maximum of 45 minutes on physical devices and 60 minutes on virtual devices. The retry count (maxTestReruns) defaults to 0, with a maximum of 10.
Update (September 2026)
This post was written with the tooling of 2025-03-26; after 18 months there are concrete changes in the Android Emulator and the Xcode/iOS toolchain, while the API/limit definitions in the Gradle Managed Devices and Firebase Test Lab docs stayed the same — only the page timestamps were updated:
- Gradle Managed Devices doc timestamped January 16, 2026: the API 27+ requirement, ATD's API-30-only support, and the hardware-render screenshot limit are unchanged.
- Android Emulator 36.5.10 (April 2, 2026): Pixel 10, 10 Pro, 10 Pro XL, and 10 Pro Fold AVD profiles were added; also came zero-configuration multi-device networking.
- Android Emulator 36.6.11 (June 2, 2026): minimum RAM for phone AVDs was raised to 4 GB starting with API 37 — this makes it even harder for the emulator to simulate low-RAM behavior, and doesn't reduce the need for testing on real old/cheap devices, it reinforces it.
- Android Emulator 37.1.11 (July 30, 2026): added the Pixel 10a AVD profile.
- Xcode 27 shipped alongside iOS 27 on September 14, 2026 — the toolchain has moved two major versions forward since this post's original version.
- Market share (StatCounter, August 2026): on Android, all six versions carry at least 8% share each — fragmentation hasn't decreased; on iOS, 26.6+26.5 together reach roughly 67%, while iOS 18.7 is still in third place at 10.36%.
Conclusion
A real-device test strategy isn't a single "correct device list" — it's a continuously updated process: derive the matrix from user analytics, deliberately keep the low-end device in the matrix, scale with managed devices/cloud in CI, and run the pre-release manual smoke test on the low-end device. If you want to dig into test writing itself, check out the iOS TDD guide; to set up the CI pipeline on the Flutter side, the Flutter CI/CD (GitHub Actions + Fastlane) post is a good starting point. To dig into the memory issues you'll run into on the low-end device, see the iOS memory management guide; to shrink app size and reduce download/install friction, check the iOS app size optimization post. If you want to back your device-matrix decisions statistically too, the mobile A/B testing infrastructure post is useful; and when adding offline scenarios to your test matrix, the Flutter offline-first database guide is a good guide.
Sources
- Gradle Managed Devices — Android Developers — API level, ATD limits, timeout/rerun/sharding settings.
- Running Tests and Interpreting Results — Apple Developer — Report Navigator summary,
.xcresultsbundle, Xcode Cloud. - Test Lab — Firebase — cloud device matrix, CI integration, load-testing limit.
- Android Version Market Share — StatCounter — August 2026 Android version distribution.
- iOS Version Market Share — StatCounter — August 2026 iOS version distribution.
- Android Studio Emulator Release Notes — AVD profile additions and RAM/network changes.
- Apple Newsroom — Releases — Xcode 27 and iOS 27 release date.
Tags
iOS Development News
Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.
We respect your privacy. You can unsubscribe at any time.

