How to Test Apps on Low-End and Older Devices

Testing on flagship smartphones gives a false sense of confidence. High‑end devices hide defects that only surface when RAM, CPU, storage, or OS version are constrained. When an app runs on a phone wi

April 26, 2026 · 16 min read · Testing Guides

Why Low‑End Devices Matter

Testing on flagship smartphones gives a false sense of confidence. High‑end devices hide defects that only surface when RAM, CPU, storage, or OS version are constrained. When an app runs on a phone with 2 GB RAM, a modest quad‑core Cortex‑A53, and eMMC storage, the system behaves differently: the Dalvik/ART heap may be forced to shrink, background processes are killed more aggressively, and the UI thread gets less time slice. These conditions expose bugs such as:

These failures translate directly into user‑visible problems: crashes, unresponsive screens, corrupted data, and poor ratings. In emerging markets where low‑end hardware dominates, a single crash can drive users to abandon the app permanently. Therefore, a test strategy that explicitly targets constrained devices is not optional; it is a quality gate.

Building a Representative Low‑End Matrix

A matrix captures the combinations of hardware and software characteristics that matter for your app. Rather than testing every possible device, you select a handful that span the relevant ranges.

Defining the Constraints (RAM, CPU, Storage, OS)

Start by enumerating the resource limits you want to validate:

DimensionLow‑End ThresholdTypical Mid‑RangeHigh‑End Reference
RAM≤ 2 GB3‑4 GB≥ 6 GB
CPU cores≤ 4 (Cortex‑A53/A55)6‑8 (mix of A53/A73)8+ (A76/A78)
Storage typeeMMC 4.5/5.0UFS 2.1UFS 3.0/3.1
Free storage at install< 500 MB> 2 GB> 5 GB
Android version8.0 (API 26) – 10.0 (API 29)11.0 (API 30) – 12.0 (API 31)13.0 (API 33) +
Screen densityldpi/mdpi (120‑160 dpi)hdpi/xhdpi (240‑320 dpi)xxhdpi/xxxhdpi (480‑640 dpi)

These thresholds are not absolute; they serve as filters for selecting devices that stress the app in ways a flagship never will.

Selecting Device Models Across Generations

Pick models that are still sold or commonly found in the wild. A practical matrix might look like this:

#Device (Year)SoCRAMStorageAndroidRemarks
1Samsung Galaxy A10 (2019)Exynos 78842 GB32 GB eMMC9.0 (API 28)Popular entry‑level
2Motorola Moto G Power (2020)Snapdragon 6624 GB64 GB eMMC10.0 (API 29)Slightly higher RAM, still eMMC
3Nokia 2.4 (2020)MediaTek Helio P222 GB32 GB eMMC10.0 (API 29)Android Go variant
4Xiaomi Redmi 9A (2020)MediaTek Helio G252 GB32 GB eMMC10.0 (API 29)Budget segment
5LG K40 (2019)Snapdragon 4252 GB32 GB eMMC9.0 (API 28)Representative of older GPU
6Realme C2 (2019)MediaTek Helio A222 GB32 GB eMMC9.0 (API 28)Low‑end GPU, frequent throttling
7Emulator: pixel_2_api_28 (ARM)2 GB2 GB9.0 (API 28)Useful for CI when hardware scarce

You can expand the matrix with additional OS versions (e.g., Android 8.1 on a legacy device) to catch API‑specific bugs. Keep the total number of devices manageable—usually 5‑7 physical units plus one emulator configuration—so that a full run fits within a nightly cycle.

Using Emulators vs Real Hardware

Emulators are valuable for early‑stage regression but cannot perfectly emulate storage I/O characteristics, thermal throttling, or GPU driver quirks of low‑end silicon. Use them for:

Reserve real devices for:

A hybrid approach—run the bulk of functional checks on emulators, then a targeted subset on hardware—delivers the best trade‑off.

Core Metrics to Monitor

When exercising the matrix, collect quantitative data that signals whether the app stays within acceptable bounds under constraint.

Performance: Frame Time, Jank, CPU Utilization

Memory: Heap Growth, GC Frequency, OOM Events

Storage: Free Space Thresholds, File I/O Latency

Network: Latency, Bandwidth, Packet Loss

Stability: ANR, Crash, Process Kill, State Restoration

Table: Metric Thresholds and Alerting

MetricAcceptable Range (Low‑End)Warning ThresholdCritical ThresholdCollection Method
90th‑percentile frame time≤ 20 ms20‑30 ms> 30 msgfxinfo framestats
Jank frames per minute≤ 55‑15> 15gfxinfo
CPU utilization (app)≤ 60 %60‑80 %> 80 %top
Heap growth per minute≤ 5 MB5‑15 MB> 15 MBmeminfo
GC pause time≤ 100 ms100‑300 ms> 300 mslogcat GC
Free storage before test≥ 500 MB200‑500 MB< 200 MBdf /data
File write latency (64 KB)≤ 20 ms20‑50 ms> 50 mscustom micro‑benchmark
Network RTT (80 % percentile)≤ 300 ms300‑800 ms> 800 mstc + curl
ANR occurrences per run01‑2> 2dumpsys activity services
Crash occurrences per run01> 1logcat + tombstone
Process kill/restore success rate≥ 95 %90‑95 %< 90 %custom validation script

Set up alerts in your CI pipeline (e.g., GitHub Actions, Jenkins) to fail a build when any metric crosses the critical threshold. Warnings can be posted to a Slack channel for triage.

Manual Testing Techniques on Constrained Devices

Even with automation, certain scenarios benefit from hands‑on exploration. Manual tests let you observe subtle UI glitches, overheating, or unexpected dialogs that automated scripts might miss.

Cold Start Under Memory Pressure

  1. **Free RAM‑starved start simulates a device where many background apps consume memory.

Simulating Storage Full

  1. Storage‑full test reveals improper handling of ENOSPC.

Inducing Low Battery / Thermal Throttle

  1. Battery and heat affect CPU frequency and can expose race conditions.

Using ADB Shell Commands to Load CPU/Memory

  1. Synthetic load helps reproduce intermittent bugs that only appear under contention.

Example: Stress Test with stress or busybox

If the device lacks stress, you can compile a simple busybox statically and use it:


# Push busybox
adb push busybox /data/local/tmp/
adb shell chmod +x /data/local/tmp/busybox

# CPU load (4 workers for 30 s)
adb shell /data/local/tmp/busybox stress --cpu 4 --timeout 30s

# Memory load (2 workers, 256 MB each)
adb shell /data/local/tmp/busybox stress --vm 2 --vm-bytes 256M --timeout 30s

Record logcat before, during, and after the load to spot any anomalies.

Automated Approaches: Scripted and Autonomous

Manual checks are indispensable for discovery, but regression confidence comes from repeatable, automated execution across the matrix.

Instrumented UI Tests with Espresso/UIAutomator

Write tests that verify core flows while collecting metrics. Espresso can be extended with IdlingResource to wait for background work, and UIAutomator can capture system‑level info.


// Example Espresso test that checks login flow and records frame timing
@LargeTest
class LoginFlowTest {
    @get:Rule
    val activityRule = ActivityScenarioRule(MainActivity::class.java)

    @Test
    fun loginSuccessRecordsMetrics() {
        // Clear any stale auth state
        adbShell("pm clear com.example.app")

        // Start activity
        val scenario = ActivityScenario.launch<LoginActivity>()

        // Fill credentials
        onView(withId(R.id.username)).perform(typeText("testuser"), closeSoftKeyboard())
        onView(withId(R.id.password)).perform(typeText("Password1!"), closeSoftKeyboard())
        onView(withId(R.id.login_button)).perform(click())

        // Wait for home screen
        onView(withId(R.id.home_toolbar)).check(matches(isDisplayed()))

        // Capture gfxinfo after the flow
        val frameStats = adbShell("dumpsys gfxinfo com.example.app framestats")
        val jankCount = parseJank(frameStats)
        assertTrue("Jank count too high: $jankCount", jankCount <= 5)
    }

    private fun adbShell(cmd: String): String = 
        ProcessBuilder("adb", "shell", cmd).redirectErrorStream(true).start()
            .bufferedReader().readText()
}

Run this test on each device in the matrix via Gradle’s connectedAndroidTest task, passing a specific device serial:


./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.device=ZY223XNPPF

Setting Up a Test Lab with Firebase Test Lab or Device Farm

If maintaining a physical lab is costly, use cloud‑based device farms. They offer a range of low‑end models (often labeled “Android Go” or “Entry”). Configure a test matrix that selects devices by RAM and OS version:


# firebase-test-lab.yml
dimensions:
  - model: ["shark", "blueline"]   # Pixel 3   a) and (Pixel 3a XL) – mid‑range but still useful for OS version
  - version: ["28", "29", "30"]
  - locale: ["en", "es"]
  - orientation: ["portrait"]
# Add a custom dimension for RAM via gcloud filter

You can then invoke:


gcloud firebase test android run \
  --type instrumentation \
  --app app-debug.apk \
  --test tests.apk \
  --device model=shark,version=28,locale=en,orientation=portrait \
  --device model=blueline,version=29,locale=es,orientation=portrait

Collect the test results and pull the logcat and performance metrics artifacts for analysis.

Using ADB Monkey with Constraints

The UI/exerciser monkey can generate pseudo‑random events while you apply system load via adb shell. Combine it with a simple bash loop to enforce constraints:


#!/usr/bin/env bash
SERIAL=$1
PACKAGE=com.example.app

# Fill RAM to 80 % (adjust numbers based on total mem)
TOTAL_KB=$(adb -s $SERIAL shell cat /proc/meminfo | grep MemTotal | awk '{print $2}')
TARGET=$((TOTAL_KB * 80 / 100))
adb -s $SERIAL shell "while true; do dd if=/dev/zero of=/data/local/tmp/memfill bs=1K count=$TARGET; done &"

# Run monkey for 10 000 events
adb -s $SERIAL shell monkey -p $PACKAGE -v 10000 > monkey.log 2>&1

# Kill the memory filler
adb -s $SERIAL shell kill $(adb -s $SERIAL shell ps | grep memfill | awk '{print $2}')

Inspect monkey.log for crashes (*** lines) and ANR traces.

Example: Gradle Task for Low‑End Run

Create a Gradle task that installs the app on a set of device serials, runs a test suite, and gathers metrics:


task lowEndTest(type: Test) {
    description 'Runs instrumentation tests on low‑end device matrix'
    group 'Verification'

    def devices = ['ZY223XNPPF', 'EM0213822D', 'HT5C1X009123'] // serial numbers

    devices.each { serial ->
        android.testOptions.unitTests.includeAndroidResources = true
        def testTask = tasks.named("connected${serial.capitalize()}AndroidTest")
        testTask.dependsOn testTask
        testTask.doFirst {
            exec {
                commandLine 'adb', '-s', serial, 'shell', 'pm', 'clear', 'android.package'
            }
        }
        testTask.finalizedBy {
            exec {
                commandLine 'adb', '-s', serial, 'pull', '/data/local/tmp/test_results.xml', "${buildDir}/reports/$serial/"
            }
        }
    }
}

Run it with ./gradlew lowEndTest. The task ensures each device starts with a clean state, runs the tests, and pulls any generated JUnit XML for aggregation.

Autonomous Exploration with SUSA

SUSA can explore an app without any test scripts, which is valuable for discovering flows that developers never anticipated. To run it under constrained conditions:


# Install the agent
pip install susatest-agent

# Point SUSA at an APK and ask it to emulate a low‑end device profile
susatest explore \
  --apk ./app-release.apk \
  --device-profile lowend \   # predefined profile: 2 GB RAM, eMMC, Android 9
  --max-depth 60-min \
  --output ./susa-report

The agent will:

You can feed the generated Appium script back into your CI pipeline to verify that the problematic flow stays fixed across releases.

Cross‑Session Learning and Regression Script Generation

SUSA stores a knowledge base of visited screens and dead ends. On subsequent runs, it prioritizes unexplored areas and avoids re‑testing paths that previously caused crashes unless the build changes. This incremental learning reduces runtime while increasing coverage of edge cases that only appear after certain state mutations (e.g., after a login, after a deep link, after a push notification).

Edge Cases That Only Appear in Production

Some defects manifest only when the app runs for extended periods, interacts with other apps, or experiences real‑world variability like fluctuating network or battery states. Below are common sources of low‑end‑only failures and how to provoke them in a test environment.

Background Service Kill and Restart

Low‑RAM devices aggressively kill background services. If your app relies on a ForegroundService for music playback or location updates, the system may restart it with a null intent, causing a NullPointerException.

Test:

  1. Start your service via adb shell am startservice.
  2. Fill RAM with a memory‑hog until the service disappears from adb shell dumpsys activity services | grep YourService.
  3. Send a broadcast that would normally be handled by the service (e.g., location update).
  4. Verify that the service either gracefully handles the restart or logs a clear error without crashing.

Locale/Font Scaling on Small Screens

Devices with low density screens often have users who enable large font sizes or display scaling. Layouts that rely on hard‑coded dp values can overflow, leading to clipped text or overlapped buttons.

Test:

Accessibility Service Interference

TalkBack or Switch Access can inject additional events that interfere with touch handling, especially when debounce logic is missing.

Test:

Secure KeyStore Delays on Old Crypto

Older devices may lack hardware-backed keystore, forcing cryptographic operations into software, which can be slow enough to trigger ANRs if done on the UI thread.

Test:

OTA Update Interruptions

An over‑the‑air system update that fails mid‑flash can leave the device in a partitioned state where some apps have mismatched native libraries.

Test (simulated):

Example: ANR Caused by SQLite on Slow eMMC

A common pattern is to perform a heavy INSERT OR REPLACE on the UI thread while the device’s eMMC exhibits write latency > 30 ms.

Reproduction steps:

  1. Simulate slow eMMC by using dd if=/dev/zero of=/data/local/tmp/fakeemmc bs=1M count=1024 oflag=direct to monopolize the I/O scheduler.
  2. In your app, trigger a bulk insert of 5 000 rows into a local SQLite database from a button click.
  3. Monitor logcat for the ANR in com.example.app (BroadcastReceiver) trace.
  4. Fix: move the database work to a Room coroutine or IntentService.

By deliberately reproducing these conditions, you can assert that your defensive coding (try/catch, off‑thread work, state checks) holds up under real‑world stress.

Checklist for Low‑End Release Gate

A concise, actionable list helps teams verify that a build satisfies low‑end quality before promotion to staging or production.

Pre‑Build Checks

Test Execution Checklist

Post‑Run Analysis

Release Decision Criteria

Takeaways and Future‑Proofing

Testing on low‑end and older hardware is not a niche activity; it is a fundamental part of delivering a reliable product to the majority of Android users, especially in markets where device renewal cycles are long. The effort invested up front pays off in fewer post‑release incidents, better Play Store ratings, and lower support costs.

Designing for Low‑End from the Start

Adopt a “constraint‑first” mindset during architecture discussions:

Continuous Monitoring in Production

Even the most thorough pre‑release matrix cannot anticipate every field condition. Implement lightweight telemetry that reports:

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free