A measured Linux localhost lab comparing subprocess.run with Popen and communicate for tiny process spawns and captured stdout.
4 min read
Spawn a tiny process many times: subprocess.run vs Popen + communicate. This lab reports launches/s for /bin/true, captured echo, and a short seq stdout capture on Linux localhost.
Related links:
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Affiliates: 0. No Docker. No shell=True.
Verdict up front: Popen+/bin/true ~1745.5 launches/s vs run ~1610.6. With pipes: Popen ~1499.8 vs run(capture_output=True) ~1309.5. Echo capture: run ~1136.3 ≈ Popen ~1177.9.
| Arm | Pattern |
|---|---|
run(["/bin/true"]) | high-level wait |
Popen + communicate | lower-level |
run(..., capture_output=True) | pipes via run |
Popen with PIPEs | explicit pipes |
| echo / seq | small stdout capture |
Script: lab-evidence/95-subprocess-run-vs-popen/results/run_lab.py.
| Arm | launches/s |
|---|---|
Popen /bin/true | 1745.5 |
run /bin/true | 1610.6 |
| Popen true + pipes | 1499.8 |
| run true capture | 1309.5 |
| Popen echo communicate | 1177.9 |
| run echo capture | 1136.3 |
| run seq 1..500 | 1065.0 |
| Popen seq 1..500 | 998.8 |
run ≈ Popen+communicate for these tiny commands — convenience tax is small (~1.08× at best for bare true).capture_output=True / PIPEs cost more than a no-pipe spawn (true: ~1610.6 → ~1309.5 launches/s).run for one-shot commands; use Popen when you need streaming, overlap, or custom pipe wiring./bin/true 1000× is still an anti-pattern — batch work inside one process when you can.subprocess.run is the recommended API for most cases. This lab’s numbers support that: you are not leaving meaningful launches/s on the table versus hand-rolled Popen for fire-and-wait. Reach for Popen when run cannot express the lifecycle.
On seq 1 500, run edged Popen (~1065.0 vs ~998.8 launches/s). Noise and helper overhead dominate at this scale; treat them as tied. The durable lesson is pipe setup cost, not a permanent ranking.
Opening pipes for a command that writes nothing (/bin/true) still pays pipe setup. Only request capture_output / PIPEs when you will read stdout/stderr. For status-only helpers, bare run/Popen.wait stays leaner (~1611 vs ~1309 launches/s on this box).
run always waits for the child to exit before returning. If you must read partial stdout while the child still runs, or multiplex several children, Popen is the API. This microbench never overlaps children — it measures spawn+wait convenience — so treat streaming designs as a separate profile.
Pass argument lists, not interpolated shell strings. Both run and Popen are safe when shell=False (default) and argv is controlled. Throughput rankings do not change that rule.
shell=True with untrusted strings.communicate / threads.check=True / return codes.Evidence: summary.json, summary.txt.
One Linux box. /bin/true, /bin/echo, seq only. Not posix_spawn tuning, not shell pipelines.
If a loop calls run hundreds of times for tiny helpers, consider one long-lived worker process or an in-process function instead — spawn overhead will dominate any run vs Popen delta.
Popen ~1745.5 /s vs run ~1610.6 /s for /bin/true; capture paths sit lower (~1309.5–1499.8). Default to subprocess.run; use Popen when you need lower-level control — not for a few percent on tiny spawns.
Lab evidence
Lab 1 Oct 2026 IST. Python 3.13.5. /bin/true: Popen 1745.5/s vs run 1610.6/s; capture run 1309.5/s vs Popen pipes 1499.8/s. Echo≈tied. Affiliates: 0. Evidence: lab-evidence/95-subprocess-run-vs-popen/.
Hands-on platform.platform vs os.uname host inventory lab: real ops/s plus cache notes, measured on Linux localhost today in this hands-on lab for SREs.
Summary1 Oct 20264 min read
Hands-on signal SIGUSR1 vs threading.Event wakeup lab: real p50 latency in microseconds, measured on Linux localhost today in this hands-on lab for SREs.
Summary1 Oct 20264 min read
Hands-on cmath vs math.hypot magnitude ops lab: real ops/s for abs, polar, and phase, measured on Linux localhost today in this hands-on lab for SREs.
Summary1 Oct 20263 min read