How to run Appium tests across devices in multiple locations—without the complexity of traditional Selenium Grid.


You’ve got Appium working on your local machine. Tests run fine on the phone plugged into your laptop.

Now you need to:

  • Run tests on devices in your CI server room
  • Let remote team members access the same device pool
  • Execute parallel tests across 10+ devices
  • Keep everything stable when devices disconnect and reconnect

Welcome to the world of distributed Appium testing. It’s where most teams hit a wall.

The traditional approach—Selenium Grid with Appium nodes—works, but it’s complex. You’re managing hub configurations, node JSON files, port conflicts, and the inevitable “device not found” errors.

This guide covers three approaches, from simplest to most powerful:

  1. Appium Device Farm Plugin — Easiest setup for local parallel testing
  2. Selenium Grid + Appium Nodes — Traditional approach for distributed teams
  3. DeviceLab P2P — Remote device access without infrastructure complexity

Let’s build this step by step.


The Core Problem: Appium Wasn’t Built for Distribution

Appium is a fantastic automation framework. But out of the box, it assumes:

  • Devices are physically connected to the machine running Appium
  • One Appium server handles one device at a time (mostly)
  • Your test client and Appium server are on the same network

When you try to scale beyond this, you hit friction:

Challenge Why It Happens
Port conflicts Each Appium instance needs unique ports (systemPort, wdaLocalPort)
Device allocation No built-in mechanism to reserve devices for specific tests
Remote access Devices connected to Machine A aren’t visible to Machine B
Health monitoring Disconnected devices cause cryptic failures
Parallel execution Requires orchestration that Appium doesn’t provide natively

Every solution to distributed Appium testing addresses these gaps differently.


Option 1: Appium Device Farm Plugin (Simplest)

If your devices are all connected to one machine and you just need parallel execution, the Appium Device Farm plugin is the fastest path.

What It Does

According to the official documentation:

Setup (5 minutes)

Prerequisites:

Install the plugin:

Per the Appium plugins documentation:

bash
# Install the device farm plugin
appium plugin install --source=npm appium-device-farm

# Optional: Install dashboard for visual monitoring
appium plugin install --source=npm appium-dashboard

Start Appium with the plugin:

From the Device Farm setup guide:

bash
appium server -ka 800 \
  --use-plugins=device-farm,appium-dashboard \
  -pa /wd/hub \
  --plugin-device-farm-platform=both

Access the dashboard:

Navigate to localhost:4723/device-farm once the Appium server is started.

Configuration Options

Create a server-config.json for more control. From the npm package documentation:

json
{
  "server": {
    "port": 4723,
    "plugin": {
      "device-farm": {
        "platform": "both",
        "iosDeviceType": "both",
        "androidDeviceType": "both",
        "skipChromeDownload": true,
        "newCommandTimeout": 120
      }
    }
  }
}

Start with the config (source):

bash
appium server -ka 800 \
  --use-plugins=device-farm \
  --config ./server-config.json \
  -pa /wd/hub

Test Code (No Changes Needed)

The beauty of Device Farm plugin: your existing Appium tests work without modification. Just point them at the server. Example from Blibli Tech Blog:

java
// Java example
UiAutomator2Options options = new UiAutomator2Options();
options.setPlatformName("Android");
options.setAutomationName("UiAutomator2");
// Don't specify UDID - Device Farm allocates automatically

AndroidDriver driver = new AndroidDriver(
    new URL("http://localhost:4723/wd/hub"),
    options
);
python
# Python example
from appium import webdriver
from appium.options.android import UiAutomator2Options

options = UiAutomator2Options()
options.platform_name = "Android"
options.automation_name = "UiAutomator2"
# No UDID needed

driver = webdriver.Remote("http://localhost:4723/wd/hub", options=options)

Parallel Execution with TestNG

From SW Test Academy’s tutorial:

xml
<!-- testng.xml -->
<suite name="Parallel Suite" parallel="tests" thread-count="4">
    <test name="Test on Device 1">
        <classes>
            <class name="com.example.LoginTest"/>
        </classes>
    </test>
    <test name="Test on Device 2">
        <classes>
            <class name="com.example.CheckoutTest"/>
        </classes>
    </test>
</suite>

Limitations

Per the Device Farm GitHub repository:

Best for: Local parallel testing on a single machine with 2-10 devices.


Option 2: Selenium Grid + Appium Nodes (Traditional)

For distributed testing across multiple machines, the traditional approach is Selenium Grid as a hub with Appium servers as nodes. Per the Selenium documentation:

“Selenium Grid allows the execution of WebDriver scripts on remote machines by routing commands sent by the client to remote browser instances.”

Architecture Overview

From BrowserStack’s Selenium Grid tutorial:

“Hub is a server that accepts access requests from the WebDriver client, routing the JSON test commands to the remote drives on nodes. It takes instructions from the client and executes them remotely on the various nodes in parallel.”

┌─────────────────────────────────────────────────────────┐
│                    Selenium Grid Hub                     │
│                   (Central Coordinator)                  │
│                    localhost:4444                        │
└─────────────────────────────────────────────────────────┘
                           │
          ┌────────────────┼────────────────┐
          │                │                │
          ▼                ▼                ▼
   ┌─────────────┐  ┌─────────────┐  ┌─────────────┐
   │ Appium Node │  │ Appium Node │  │ Appium Node │
   │   :4723     │  │   :4724     │  │   :4725     │
   │   Pixel 7   │  │  iPhone 14  │  │  Galaxy S23 │
   │  Machine A  │  │  Machine B  │  │  Machine A  │
   └─────────────┘  └─────────────┘  └─────────────┘

Step 1: Start the Selenium Grid Hub

Download Selenium Server and start the hub. Per Selenium’s getting started guide:

bash
# Start the hub on port 4444
java -jar selenium-server-4.x.x.jar hub --port 4444

The Grid console is available at http://localhost:4444/ui. As noted in the Selenium Grid documentation:

“By default, the server will listen for RemoteWebDriver requests on http://localhost:4444.”

Step 2: Configure Appium Nodes

Each device needs a node configuration file. From SIXT Tech’s Appium guide:

“In order to register a device with the grid we need to create a node config file which is a json file.”

Create node-pixel7.json:

json
{
  "capabilities": [
    {
      "platformName": "Android",
      "appium:automationName": "UiAutomator2",
      "appium:deviceName": "Pixel 7",
      "appium:udid": "DEVICE_UDID_HERE",
      "maxInstances": 1
    }
  ],
  "configuration": {
    "cleanUpCycle": 2000,
    "timeout": 30000,
    "proxy": "org.openqa.grid.selenium.proxy.DefaultRemoteProxy",
    "url": "http://192.168.1.100:4723/wd/hub",
    "host": "192.168.1.100",
    "port": 4723,
    "maxSession": 1,
    "register": true,
    "registerCycle": 5000,
    "hubHost": "192.168.1.50",
    "hubPort": 4444
  }
}

For iOS devices, create node-iphone14.json:

json
{
  "capabilities": [
    {
      "platformName": "iOS",
      "appium:automationName": "XCUITest",
      "appium:deviceName": "iPhone 14",
      "appium:udid": "00008030-000A12345678901E",
      "maxInstances": 1
    }
  ],
  "configuration": {
    "url": "http://192.168.1.101:4724/wd/hub",
    "host": "192.168.1.101",
    "port": 4724,
    "maxSession": 1,
    "register": true,
    "hubHost": "192.168.1.50",
    "hubPort": 4444
  }
}

Step 3: Start Appium Nodes

For each device, start an Appium server with unique ports. From SIXT Tech’s guide:

bash
# Android device on Machine A
appium -p 4723 \
  --nodeconfig ./node-pixel7.json \
  --relaxed-security \
  --default-capabilities \
  '{"udid":"DEVICE_UDID","systemPort":8200}'
bash
# iOS device on Machine B (requires Mac)
appium -p 4724 \
  --nodeconfig ./node-iphone14.json \
  --relaxed-security \
  --default-capabilities \
  '{"udid":"00008030-000A12345678901E","wdaLocalPort":8100}'

Critical ports to make unique per device:

Per the UiAutomator2 driver documentation and XCUITest driver documentation:

Platform Port Purpose
Both Appium server port (-p) Main Appium communication
Android systemPort UiAutomator2 server (default 8200-8299)
Android chromedriverPort Chrome/WebView automation
iOS wdaLocalPort WebDriverAgent
iOS mjpegServerPort Screen streaming

Step 4: Run Tests Against the Grid

Point your tests at the Grid hub, not individual Appium servers. From BrowserStack’s parallel execution guide:

java
// Java - Tests go through Grid hub
DesiredCapabilities caps = new DesiredCapabilities();
caps.setCapability("platformName", "Android");
caps.setCapability("appium:automationName", "UiAutomator2");
caps.setCapability("appium:app", "/path/to/app.apk");
// Grid routes to available matching device

AndroidDriver driver = new AndroidDriver(
    new URL("http://grid-hub:4444/wd/hub"),
    caps
);

The Problems with This Approach

From HeadSpin’s guide on parallel Appium testing:

“Unfortunately, this isn’t everything we have to worry about. Appium orchestrates a host of services under the hood to facilitate communication between different subsystems and drivers.”

Common issues include:

  1. Node registration failures — If the hub restarts, nodes don’t always re-register cleanly
  2. Stale sessions — Crashed tests leave devices in limbo
  3. No health checks — Grid doesn’t know if a device is actually responsive
  4. Complex networking — Firewalls, NAT, and VPNs make cross-location setups painful
  5. Maintenance burden — Every device needs a config file, unique ports, and monitoring

For more on managing 10+ devices, see our detailed guide.

Best for: Teams with DevOps expertise who need full control over infrastructure.


Option 3: DeviceLab P2P (Remote Without Complexity)

What if you could access devices in multiple locations without running Selenium Grid, managing ports, or dealing with network configuration?

That’s the premise of DeviceLab. Your devices connect directly to remote users via P2P WebRTC connections. No central server routing traffic. No port forwarding. No VPNs.

How It Works

┌──────────────────┐         WebRTC P2P          ┌──────────────────┐
│   Your Laptop    │◄───────────────────────────►│  Device in Lab   │
│   (Test Client)  │    Direct Connection        │  (Running Agent) │
└──────────────────┘                             └──────────────────┘
                              │
                              │ Signaling only
                              ▼
                    ┌──────────────────┐
                    │  DeviceLab Cloud │
                    │  (Coordination)  │
                    └──────────────────┘

Key difference: Your app binaries and test data flow directly between your machine and the device. They never pass through DeviceLab servers.

Setup

On the machine with devices:

  1. Install the DeviceLab agent
  2. Connect devices via USB
  3. Devices appear in your DeviceLab dashboard

On your test machine:

  1. Get your device’s remote Appium URL from the dashboard
  2. Point your tests at that URL
java
// Connect to a device in your Bangalore office from your laptop in SF
AndroidDriver driver = new AndroidDriver(
    new URL("https://your-device.devicelab.dev/wd/hub"),
    capabilities
);

What You Get

Feature Selenium Grid DeviceLab
Cross-location access Complex (VPN/tunnels) Built-in
Device health monitoring Manual Automatic
Session recording Manual setup Built-in
Port management Manual per device Automatic
Data privacy Your network P2P (never touches cloud)
Setup time Hours to days Minutes

When DeviceLab Makes Sense

  • Devices in multiple physical locations
  • Remote team members need device access
  • Security requirements prohibit sending apps to cloud farms
  • You want device lab benefits without DevOps overhead

Best for: Teams who want distributed device access without infrastructure complexity.


Capability Reference for Distributed Testing

Regardless of which approach you use, these capabilities are essential for distributed Appium testing.

Android Capabilities

From the UiAutomator2 driver documentation and LambdaTest’s capability guide:

java
UiAutomator2Options options = new UiAutomator2Options();

// Essential for device targeting
options.setUdid("DEVICE_SERIAL");           // Specific device
options.setPlatformVersion("14");            // OS version matching
options.setDeviceName("Pixel 7");            // For logging/identification

// Essential for parallel execution (per UiAutomator2 docs)
options.setSystemPort(8200);                 // Unique per device (default 8200-8299)
options.setChromedriverPort(9515);           // Unique if testing WebViews
options.setAdbExecTimeout(60000);            // Longer for remote devices

// Recommended for stability
options.setNewCommandTimeout(120);           // Prevent premature session death
options.setNoReset(false);                   // Clean state between tests
options.setAutoGrantPermissions(true);       // Avoid permission dialogs

As noted in LambdaTest’s documentation:

“By default, any of the 8200-8299 free port numbers will be used. But if you want to execute your tests in parallel, you must ensure that you provide a unique systemPort number capability.”

iOS Capabilities

From the XCUITest driver documentation:

java
XCUITestOptions options = new XCUITestOptions();

// Essential for device targeting
options.setUdid("00008030-000A12345678901E");
options.setPlatformVersion("17.0");
options.setDeviceName("iPhone 14 Pro");

// Essential for parallel execution
options.setWdaLocalPort(8100);              // Unique per device
options.setMjpegServerPort(9100);           // Unique for screen streaming
options.setWebkitDebugProxyPort(27753);     // Unique for Safari debugging

// Recommended for stability
options.setNewCommandTimeout(120);
options.setWdaLaunchTimeout(120000);        // WDA can be slow to start
options.setWdaConnectionTimeout(60000);
options.setCommandTimeouts("{}");

Dynamic Port Allocation

When running many parallel tests, hardcoding ports doesn’t scale. From HeadSpin’s parallel testing guide:

“udid: if you don’t include this capability, the driver will attempt to use the first device in the list returned by ADB. This could result in multiple sessions targeting the same device, which is not a desirable situation.”

Use dynamic allocation:

java
public class PortManager {
    private static final AtomicInteger systemPortCounter = new AtomicInteger(8200);
    private static final AtomicInteger wdaPortCounter = new AtomicInteger(8100);

    public static int getNextSystemPort() {
        return systemPortCounter.getAndIncrement();
    }

    public static int getNextWdaPort() {
        return wdaPortCounter.getAndIncrement();
    }
}

// In your test setup
options.setSystemPort(PortManager.getNextSystemPort());

Or use truly random available ports:

java
public static int findFreePort() {
    try (ServerSocket socket = new ServerSocket(0)) {
        return socket.getLocalPort();
    } catch (IOException e) {
        throw new RuntimeException("No free port available", e);
    }
}

CI/CD Integration Patterns

GitHub Actions with Self-Hosted Devices

yaml
name: Mobile Tests
on: [push]

jobs:
  android-tests:
    runs-on: self-hosted  # Your machine with devices
    steps:
      - uses: actions/checkout@v4

      - name: Start Appium with Device Farm
        run: |
          appium server -ka 800 \
            --use-plugins=device-farm \
            -pa /wd/hub \
            --plugin-device-farm-platform=android &
          sleep 10  # Wait for server

      - name: Run Tests
        run: ./gradlew connectedAndroidTest

      - name: Stop Appium
        if: always()
        run: pkill -f appium || true

Jenkins with Parallel Device Execution

groovy
pipeline {
    agent any

    stages {
        stage('Parallel Device Tests') {
            parallel {
                stage('Android Tests') {
                    steps {
                        sh '''
                            ./gradlew test \
                              -PappiumUrl=http://device-farm:4723/wd/hub \
                              -Pplatform=android
                        '''
                    }
                }
                stage('iOS Tests') {
                    steps {
                        sh '''
                            ./gradlew test \
                              -PappiumUrl=http://mac-mini:4724/wd/hub \
                              -Pplatform=ios
                        '''
                    }
                }
            }
        }
    }
}

GitLab CI with DeviceLab

yaml
mobile-tests:
  stage: test
  script:
    - |
      # DeviceLab provides stable URLs - no infrastructure to manage
      ./gradlew test \
        -PappiumUrl=$DEVICELAB_ANDROID_URL \
        -Pplatform=android
  parallel:
    matrix:
      - DEVICE: [pixel-7, galaxy-s23, oneplus-11]

Troubleshooting Distributed Appium

“Could not start a new session”

Symptoms: Session creation fails with timeout or connection refused.

Causes and fixes:

bash
# 1. Check if Appium is running
curl http://localhost:4723/status

# 2. Check if device is connected
adb devices  # Android
idevice_id -l  # iOS

# 3. Check for port conflicts
lsof -i :4723
lsof -i :8200  # systemPort

# 4. Restart ADB server (Android)
adb kill-server && adb start-server

If you’re seeing tests pass locally but fail in CI, check for environment differences.

“Device not found” in Grid

Symptoms: Grid shows node but tests fail to find device.

Causes (from OneFootball’s distributed testing guide):

“If your Appium server is running on a different machine to your Selenium Grid server, make sure you use an external name/IP address in your host & url docs; localhost and 127.0.0.1 will prevent Selenium Grid from connecting correctly.”

  1. UDID mismatch between node config and actual device
  2. Device disconnected after node registration
  3. ADB not recognizing device

Fixes:

bash
# Verify UDID matches
adb devices  # Check actual UDID

# Re-register node
# Restart Appium with correct UDID in --default-capabilities

Parallel tests interfering with each other

Symptoms: Tests randomly fail when running in parallel but pass individually.

Causes (from HeadSpin’s guide):

“There are thus a handful of capabilities that need to be included to make sure no system resource conflicts exist between your different test sessions.”

  1. Port conflicts (systemPort, wdaLocalPort, etc.)
  2. Tests modifying shared state on device
  3. Insufficient device resources (memory/CPU)

Fixes:

java
// Ensure unique ports
options.setSystemPort(findFreePort());
options.setChromedriverPort(findFreePort());

// Reset app state between tests
options.setNoReset(false);
options.setFullReset(true);  // Reinstall app each time

iOS “WebDriverAgent installation failed”

Symptoms: XCUITest driver fails to install WDA on device.

Causes (from Palo-IT’s iOS Appium guide):

“WebDriverAgent is an iOS Webdriver server that can be used to remote control iOS devices… This application must be signed in order to be installed on iOS devices.”

  1. Code signing issues
  2. Provisioning profile expired
  3. Device not trusted

Fixes:

From AWS Device Farm documentation:

“In order to run Appium tests on iOS devices, the use of WebDriverAgent is required. This application must be signed in order to be installed on iOS devices.”

bash
# Check provisioning profiles
security find-identity -v -p codesigning

# Rebuild WDA manually
cd ~/.appium/node_modules/appium-xcuitest-driver/node_modules/appium-webdriveragent
xcodebuild -project WebDriverAgent.xcodeproj \
  -scheme WebDriverAgentRunner \
  -destination 'id=DEVICE_UDID' \
  test

Comparison: Which Approach Should You Use?

Criteria Device Farm Plugin Selenium Grid DeviceLab
Setup complexity Low (5 min setup) High (requires DevOps) Low
Devices on one machine Yes Yes Yes
Devices across locations No Yes (complex) Yes (simple)
Remote team access No Yes (VPN needed) Yes (built-in)
Port management Automatic Manual Automatic
Device health monitoring Basic Manual Automatic
Data privacy Local Your network P2P (data never in cloud)
Cost Free Free (+ DevOps time) $99/device/month
Best for Local parallel Full control Distributed teams

The Path Forward

Start simple:

  1. If devices are local: Use Appium Device Farm plugin. 5-minute setup.
  2. If you need distribution but have DevOps capacity: Build Selenium Grid infrastructure.
  3. If you need distribution without the complexity: Try DeviceLab.

Cloud options include Kobiton (per-minute pricing, mobile-focused) and BrowserStack (per-session, broader coverage).

For teams planning to grow, see our guide on scaling from 0 to 100 devices.

The goal isn’t the most sophisticated setup. It’s the setup that lets you run reliable tests across real devices without consuming engineering time on infrastructure.


Running Appium tests across distributed devices? DeviceLab gives you remote device access via P2P—your tests connect directly to devices anywhere, and your data never touches our servers. First device free. $99/device/month after that.