[{"data":1,"prerenderedAt":20},["ShallowReactive",2],{"blog:post:en:simulate-mobile-network-conditions-proxies-app-perf-testing":3},{"slug":4,"lang":5,"title":6,"summary":7,"date":8,"tags":9,"tag_slugs":15,"thumbnail_url":16,"translations":17,"body":18,"asset_base":19},"simulate-mobile-network-conditions-proxies-app-perf-testing","en","Simulate Mobile Network Conditions with Proxies for App Performance Testing","Learn how to combine mobile and residential proxies with traffic shaping to emulate 3G, 4G, and 5G network profiles, enabling realistic performance testing for mobile apps and web services.","2026-09-10",[10,11,12,13,14],"mobile-proxy","network-simulation","performance-testing","traffic-shaping","ci-cd",[10,11,12,13,14],"https://blog-api.ro-proxy.com/api/blog/posts/simulate-mobile-network-conditions-proxies-app-perf-testing/thumbnail.svg?lang=en",[5],"## Why Simulate Mobile Network Conditions?\n\nMobile users experience wildly different network realities: spotty 3G on a commuter train, bursty 4G in a city centre, or ultra‑low‑latency 5G in a stadium. If you only test against a stable office Wi‑Fi connection, you miss latency spikes, packet loss, and bandwidth throttling that cause real‑world crashes, slow renders, and failed API calls. Simulating those conditions early lets you:\n\n* Detect timeout‑related bugs before they reach production.\n* Optimize asset sizes and caching strategies for low‑bandwidth links.\n* Validate retry/back‑off logic under realistic jitter.\n* Provide credible performance numbers to stakeholders.\n\nProxies are the perfect control plane because they sit between your test client and the target, allowing you to inject latency, limit bandwidth, and drop packets without touching the application code.\n\n## Proxy Types That Enable Network Emulation\n\n| Proxy type | Typical use case | Built‑in shaping? | When to choose |\n|------------|------------------|-------------------|----------------|\n| **Mobile (4G/5G) proxies** | Real carrier IPs, genuine radio characteristics | Often yes (provider dashboard) | You need authentic ASN/geo and carrier‑level latency |\n| **Residential proxies** | Broad ISP diversity, good for geo‑targeting | Rarely, but you can shape locally | Cost‑effective when you only need IP diversity |\n| **Datacenter proxies + local shaping** | High throughput, low cost | No – you add shaping yourself | Internal load‑test labs where you control the host |\n\nPick the combination that matches your budget and realism requirements. For most mobile‑app teams, a small pool of mobile proxies plus local `tc`/`netem` shaping gives the best fidelity.\n\n## Setting Up a Proxy With Traffic Shaping\n\nYou have three practical ways to impose network profiles.\n\n### Option 1: Provider‑Side Throttling (RoProxy Mobile Proxies)\n\nRoProxy’s mobile‑proxy API lets you attach a *network profile* to each session. Example `curl` to start a 4G‑typical session:\n\n```bash\ncurl -X POST https://api.roproxy.com/v1/sessions \\\n  -H \"Authorization: Bearer $ROPROXY_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"proxy_type\": \"mobile\",\n        \"geo\": \"US\",\n        \"network_profile\": \"4g_typical\",\n        \"sticky\": true\n      }'\n```\n\nThe response includes `proxy_host`, `proxy_port`, `username`, `password`. Use those credentials directly in your test scripts – no extra tooling required.\n\n### Option 2: Local Traffic Shaping with `tc`/`netem` (Linux)\n\nIf you run your own proxy (e.g., a tiny `mitmproxy` instance) on a Linux box, you can shape the outbound interface:\n\n```bash\n# Create a root qdisc on eth0\nsudo tc qdisc add dev eth0 root handle 1: htb default 12\n\n# 4G typical: 12 Mbps down, 5 Mbps up, 30 ms RTT, 0.5 % loss\nsudo tc class add dev eth0 parent 1: classid 1:12 htb rate 12mbit ceil 12mbit\nsudo tc qdisc add dev eth0 parent 1:12 handle 12: netem delay 30ms loss 0.5%\n\n# Apply to traffic sourced from the proxy user (uid 1001)\nsudo tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match u32 0 0 flowid 1:12\n```\n\nAdjust `rate`, `delay`, `loss` to match 3G (`1.5mbit`, `100ms`, `1%`) or 5G (`50mbit`, `10ms`, `0.1%`). Remove with `sudo tc qdisc del dev eth0 root` when done.\n\n### Option 3: Programmable Shaping in Code\n\nWhen you cannot touch the host (e.g., serverless CI runners), shape inside the client process.\n\n#### Python – custom `HTTPAdapter` with `urllib3`\n\n```python\nimport time\nimport requests\nfrom requests.adapters import HTTPAdapter\nfrom urllib3.util.retry import Retry\n\nclass ShapedAdapter(HTTPAdapter):\n    \"\"\"Adds fixed latency and bandwidth throttling per request.\"\"\"\n    def __init__(self, latency_ms=100, kbps=500, *args, **kwargs):\n        self.latency = latency_ms / 1000.0          # seconds\n        self.chunk_size = int(kbps * 1024 / 8)      # bytes per second\n        super().__init__(*args, **kwargs)\n\n    def send(self, request, **kwargs):\n        # Simulate RTT latency\n        time.sleep(self.latency)\n        resp = super().send(request, **kwargs)\n        # Throttle response body streaming\n        if self.chunk_size and hasattr(resp.raw, 'stream'):\n            original_stream = resp.raw.stream\n            def throttled(amt=None):\n                for chunk in original_stream(amt):\n                    yield chunk\n                    time.sleep(len(chunk) / self.chunk_size)\n            resp.raw.stream = throttled\n        return resp\n\nsession = requests.Session()\nretries = Retry(total=3, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504])\nsession.mount('http://', ShapedAdapter(latency_ms=150, kbps=800, max_retries=retries))\nsession.mount('https://', ShapedAdapter(latency_ms=150, kbps=800, max_retries=retries))\n\nproxy = \"http://user:pass@mobile-us.roproxy.com:8000\"\nresp = session.get(\"https://api.example.com/v1/feed\", proxies={\"http\": proxy, \"https\": proxy}, timeout=10)\nprint(resp.status_code, resp.elapsed.total_seconds())\n```\n\n#### Node.js – `http-proxy-agent` + `net` socket delay\n\n```js\nconst http = require('http');\nconst { HttpProxyAgent } = require('http-proxy-agent');\nconst { once } = require('events');\n\nconst proxy = 'http://user:pass@mobile-us.roproxy.com:8000';\nconst agent = new HttpProxyAgent(proxy);\n\nfunction requestWithShaping(url, latencyMs = 120, kbps = 600) {\n  return new Promise((resolve, reject) => {\n    const req = http.request(url, { agent }, async (res) => {\n      // Simulate RTT\n      await new Promise(r => setTimeout(r, latencyMs));\n      const chunks = [];\n      const throttle = kbps * 1024 / 8; // bytes per sec\n      res.on('data', (chunk) => {\n        chunks.push(chunk);\n        // Simple per‑chunk throttle\n        const delay = chunk.length / throttle * 1000;\n        await new Promise(r => setTimeout(r, delay));\n      });\n      res.on('end', () => resolve(Buffer.concat(chunks)));\n    });\n    req.on('error', reject);\n    req.end();\n  });\n}\n\n(async () => {\n  const body = await requestWithShaping('https://api.example.com/v1/feed');\n  console.log('bytes:', body.length);\n})();\n```\n\nBoth snippets let you parameterise latency and bandwidth per test case, making it trivial to sweep a matrix of network profiles.\n\n## Integrating Into Automated Test Suites\n\n### Pytest + Requests (Python)\n\n```python\nimport pytest\nimport statistics\n\nPROFILES = [\n    (\"3g\", 1500, 500),   # latency ms, kbps\n    (\"4g\", 80, 5000),\n    (\"5g\", 20, 20000),\n]\n\n@pytest.mark.parametrize(\"profile,latency,kbps\", PROFILES)\ndef test_api_latency(profile, latency, kbps):\n    adapter = ShapedAdapter(latency_ms=latency, kbps=kbps)\n    s = requests.Session()\n    s.mount('https://', adapter)\n    proxy = \"http://user:pass@mobile-us.roproxy.com:8000\"\n    latencies = []\n    for _ in range(5):\n        r = s.get(\"https://api.example.com/v1/ping\", proxies={\"https\": proxy}, timeout=5)\n        assert r.status_code == 200\n        latencies.append(r.elapsed.total_seconds() * 1000)\n    avg = statistics.mean(latencies)\n    # Assert that observed latency stays within 2× the injected latency\n    assert avg \u003C latency * 2, f\"{profile} avg {avg:.0f} ms > {latency*2} ms\"\n```\n\n### Playwright (Node.js) – Browser‑Level Network Conditions\n\nPlaywright can combine a proxy with its built‑in network emulation:\n\n```js\nconst { chromium } = require('playwright');\n\nconst profiles = {\n  '3g': { download: 1.5 * 1024 * 1024, upload: 768 * 1024, latency: 150 },\n  '4g': { download: 12 * 1024 * 1024, upload: 5 * 1024 * 1024, latency: 30 },\n  '5g': { download: 50 * 1024 * 1024, upload: 20 * 1024 * 1024, latency: 10 },\n};\n\nasync function run(profileName) {\n  const p = profiles[profileName];\n  const browser = await chromium.launch({ proxy: { server: 'http://mobile-us.roproxy.com:8000', username: 'user', password: 'pass' } });\n  const context = await browser.newContext();\n  await context.setOffline(false);\n  await context.route('**/*', route => route.continue());\n  // Emulate network\n  const client = await context.newCDPSession(await context.newPage());\n  await client.send('Network.emulateNetworkConditions', {\n    offline: false,\n    downloadThroughput: p.download,\n    uploadThroughput: p.upload,\n    latency: p.latency,\n  });\n  const page = await context.newPage();\n  const start = Date.now();\n  await page.goto('https://example.com', { waitUntil: 'networkidle' });\n  console.log(`${profileName} load time: ${Date.now() - start} ms`);\n  await browser.close();\n}\n\nfor (const name of Object.keys(profiles)) await run(name);\n```\n\n## Measuring and Asserting Performance Metrics\n\nDefine a small KPI set per profile:\n\n| KPI | Target (4G) | How to capture |\n|-----|-------------|----------------|\n| **TTFB** (ms) | ≤ 250 | `resp.elapsed` (Python) or `performance.timing.responseStart - requestStart` (browser) |\n| **Full page load** (ms) | ≤ 3000 | `page.loadEventEnd - navigationStart` (Playwright) |\n| **API error rate** | \u003C 0.5 % | Count non‑2xx over 100 requests |\n| **Retry count** | ≤ 1 per request | Instrument retry logic |\n\nCollect metrics in a CSV or push to Prometheus for trend analysis.\n\n## CI/CD Pipeline Integration\n\n### GitHub Actions (self‑hosted runner with `tc`)\n\n```yaml\nname: Mobile‑Network‑Perf\non: [push, pull_request]\njobs:\n  perf:\n    runs-on: [self‑hosted, linux]\n    steps:\n      - uses: actions/checkout@v4\n      - name: Install tc\n        run: sudo apt-get update && sudo apt-get install -y iproute2\n      - name: Apply 4G shaping\n        run: |\n          sudo tc qdisc add dev eth0 root handle 1: htb default 12\n          sudo tc class add dev eth0 parent 1: classid 1:12 htb rate 12mbit ceil 12mbit\n          sudo tc qdisc add dev eth0 parent 1:12 handle 12: netem delay 30ms loss 0.5%\n      - name: Run pytest suite\n        env:\n          ROPROXY_TOKEN: ${{ secrets.ROPROXY_TOKEN }}\n        run: pytest -q tests/perf/\n      - name: Cleanup shaping\n        if: always()\n        run: sudo tc qdisc del dev eth0 root\n```\n\nIf you lack a self‑hosted runner, use the provider‑side profile (Option 1) – no host privileges needed.\n\n## Common Pitfalls & Best Practices\n\n* **Don’t shape the control plane** – only shape traffic *after* the proxy handshake; otherwise authentication may time out.\n* **Reset shaping between test runs** – leftover `tc` rules corrupt subsequent profiles.\n* **Use sticky sessions for login flows** – rotate only after the authenticated session is established.\n* **Monitor proxy health** – a dead mobile node will masquerade as “high latency”. Implement a quick health‑check (`GET /health`) before each batch.\n* **Log the exact profile parameters** – store latency, bandwidth, loss in test artefacts for reproducibility.\n* **Avoid double‑shaping** – if the provider already enforces a profile, skip local `tc` to prevent compounding effects.\n\n## Full End‑to‑End Example (Python)\n\n```python\n#!/usr/bin/env python3\n\"\"\"Run a 3‑profile latency sweep against a target API using RoProxy mobile proxies.\"\"\"\nimport os, time, statistics, requests\nfrom requests.adapters import HTTPAdapter\nfrom urllib3.util.retry import Retry\n\nPROFILES = {\n    \"3g\":  (150, 500),\n    \"4g\":  (80, 5000),\n    \"5g\":  (20, 20000),\n}\n\nclass ShapedAdapter(HTTPAdapter):\n    def __init__(self, latency_ms, kbps, *a, **kw):\n        self.latency = latency_ms / 1000.0\n        self.rate = kbps * 1024 / 8\n        super().__init__(*a, **kw)\n    def send(self, request, **kw):\n        time.sleep(self.latency)\n        resp = super().send(request, **kw)\n        if self.rate and hasattr(resp.raw, 'stream'):\n            orig = resp.raw.stream\n            def throttled(amt=None):\n                for chunk in orig(amt):\n                    yield chunk\n                    time.sleep(len(chunk) / self.rate)\n            resp.raw.stream = throttled\n        return resp\n\ndef make_session(latency, kbps):\n    s = requests.Session()\n    retry = Retry(total=2, backoff_factor=0.3, status_forcelist=[500,502,503,504])\n    adapter = ShapedAdapter(latency, kbps, max_retries=retry)\n    s.mount('https://', adapter)\n    return s\n\ndef fetch_proxy():\n    # Assume env var ROPROXY_SESSION contains a pre‑created session JSON\n    import json, base64\n    data = json.loads(base64.b64decode(os.getenv('ROPROXY_SESSION')).decode())\n    return f\"http://{data['username']}:{data['password']}@{data['proxy_host']}:{data['proxy_port']}\"\n\ndef main():\n    proxy = fetch_proxy()\n    target = \"https://api.example.com/v1/feed\"\n    for name, (lat, bw) in PROFILES.items():\n        sess = make_session(lat, bw)\n        times = []\n        for i in range(7):\n            r = sess.get(target, proxies={\"https\": proxy}, timeout=8)\n            r.raise_for_status()\n            times.append(r.elapsed.total_seconds() * 1000)\n        avg = statistics.mean(times)\n        p95 = sorted(times)[int(0.95 * len(times))]\n        print(f\"{name:3s}  avg={avg:6.1f} ms  p95={p95:6.1f} ms  samples={len(times)}\")\n\nif __name__ == \"__main__\":\n    main()\n```\n\n## Full End‑to‑End Example (Node.js)\n\n```js\n// run-perf.mjs\nimport { HttpProxyAgent } from 'http-proxy-agent';\nimport https from 'https';\nimport { performance } from 'perf_hooks';\n\nconst PROFILES = {\n  '3g': { latency: 150, kbps: 500 },\n  '4g': { latency: 80,  kbps: 5000 },\n  '5g': { latency: 20,  kbps: 20000 },\n};\n\nconst SESSION = JSON.parse(Buffer.from(process.env.ROPROXY_SESSION, 'base64').toString());\nconst PROXY_URL = `http://${SESSION.username}:${SESSION.password}@${SESSION.proxy_host}:${SESSION.proxy_port}`;\nconst AGENT = new HttpProxyAgent(PROXY_URL);\nconst TARGET = 'https://api.example.com/v1/feed';\nconst SAMPLES = 7;\n\nasync function fetchWithShaping(latencyMs, kbps) {\n  const start = performance.now();\n  await new Promise(r => setTimeout(r, latencyMs)); // RTT simulation\n  return new Promise((resolve, reject) => {\n    const req = https.request(TARGET, { agent: AGENT }, (res) => {\n      let bytes = 0;\n      res.on('data', chunk => { bytes += chunk.length; });\n      res.on('end', () => resolve(performance.now() - start));\n    });\n    req.on('error', reject);\n    req.end();\n  });\n}\n\nasync function run() {\n  for (const [name, { latency, kbps }] of Object.entries(PROFILES)) {\n    const durations = [];\n    for (let i = 0; i \u003C SAMPLES; i++) {\n      durations.push(await fetchWithShaping(latency, kbps));\n    }\n    const avg = durations.reduce((a,b)=>a+b,0)/durations.length;\n    const p95 = durations.sort((a,b)=>a-b)[Math.floor(0.95*durations.length)];\n    console.log(`${name.padStart(3)}  avg=${avg.toFixed(1)} ms  p95=${p95.toFixed(1)} ms`);\n  }\n}\n\nrun().catch(e => { console.error(e); process.exit(1); });\n```\n\n## Summary Checklist\n\n- [ ] Choose proxy type (mobile > residential > datacenter) based on realism vs. cost.\n- [ ] Decide shaping layer: provider‑side, host‑side `tc`, or in‑code adapter.\n- [ ] Define network profiles (3G/4G/5G) with latency, bandwidth, loss.\n- [ ] Instrument test client (requests, Playwright, custom script) to apply shaping.\n- [ ] Capture KPIs: TTFB, full load, error rate, retry count.\n- [ ] Automate in CI/CD – either self‑hosted runner with `tc` or provider API.\n- [ ] Reset/clean shaping after each job to avoid cross‑contamination.\n- [ ] Log profile parameters alongside results for traceability.\n\nBy treating the proxy as a *network condition injector* rather than just an IP rotator, you gain a repeatable, programmable way to validate that your mobile apps and APIs survive the real world – before your users encounter the glitches.\n","https://blog-api.ro-proxy.com/api/blog/posts/simulate-mobile-network-conditions-proxies-app-perf-testing/assets",1790057933381]