NetAuto Academy

Advanced Python Automation for Network Engineers

Concurrent Device Operations with ThreadPoolExecutor

Advanced14 min read

What you'll learn

  • Why every capstone through Phase 3.2 has run sequentially
  • How ThreadPoolExecutor runs bounded, concurrent device operations
  • A real, measured sequential-vs-concurrent timing comparison
  • Why asyncio is deferred, and why 20 devices (not 5) are used here
🎓 Teach This Lesson

Listen to a guided explanation of this lesson.

Spoken narration isn't supported in this browser.

Every capstone so far has been sequential

[check_device(device) for device in devices] — a plain loop, one device after another. Fine for 5 devices. The cost is real once a fleet is large enough that each device's network round-trip adds up.

A deliberate exception to this curriculum's fleet size

Every capstone since Phase 2.6 has used the same 5 devices (SW1-SW5) — 5 devices can't produce a measurable timing difference between sequential and concurrent execution. This lesson uses a separate, flat, generated 20-device fleet (SIM1-SIM20) specifically to make the comparison real and visible. It's not a new simulation framework — just enough synthetic hostnames to measure a real effect.

Quick checkConcept check
Why does this lesson use 20 synthetic devices instead of the usual 5-device SW1–SW5 fleet?

Bounded concurrency, for real

sequential_vs_concurrent.py
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

SIM_HOSTNAMES = [f"SIM{i}" for i in range(1, 21)]

def get_one_state(conn, hostname):
  return conn.get_state(hostname)

# Sequential
start = time.perf_counter()
with Connection(BASE_URL, token="demo-token") as conn:
  sequential_results = [get_one_state(conn, h) for h in SIM_HOSTNAMES]
sequential_elapsed = time.perf_counter() - start

# Bounded concurrent -- at most 5 requests in flight at once
start = time.perf_counter()
with Connection(BASE_URL, token="demo-token") as conn:
  with ThreadPoolExecutor(max_workers=5) as executor:
      futures = {executor.submit(get_one_state, conn, h): h for h in SIM_HOSTNAMES}
      concurrent_results = [future.result() for future in as_completed(futures)]
concurrent_elapsed = time.perf_counter() - start

max_workers=5 bounds concurrency deliberately -- not unlimited. as_completed() yields each future as it finishes, in completion order, not submission order.

output — real, measured, not assumed
Sequential elapsed: 6.30s for 20 devices
Concurrent elapsed: 1.26s for 20 devices
Speedup: 5.01x

Real numbers from an actual run against this module's mock backend (each request carries a small, realistic simulated delay). Not a documented assumption — this exact comparison was executed and timed.

Quick checkPredict output
With max_workers=5 and 20 devices, each request taking roughly the same fixed time, what's the rough expected relationship between sequential and concurrent elapsed time?

Partial failures don't block the rest

concurrent_with_failures.py
with Connection(BASE_URL, token="demo-token") as conn:
  with ThreadPoolExecutor(max_workers=5) as executor:
      futures = {executor.submit(check_device, conn, device): device for device in devices}
      for future in as_completed(futures):
          device = futures[future]
          result = future.result()  # check_device() already returns a result, never raises
          print(f"  {device.hostname} finished -> {result.status.value}")

check_device() (Lessons 3-5) already converts every exception into an AutomationResult -- so future.result() here never itself raises. A slow or failing device shows up as a result, not a crash.

output — real, verified run against SW1-SW5
  SW1 finished -> already-compliant
SW2 finished -> validation-failed
SW5 finished -> auth-failed
SW3 finished -> validation-failed
SW4 finished -> unreachable

All 5 devices finished in 7.06s (concurrent, max_workers=5)

Real, verified output. Notice the order: SW1, SW2, and SW5 finish almost immediately; SW3 takes slightly longer (its real retry from Lesson 4); SW4 -- genuinely unreachable -- takes the longest, but never blocks the other four from finishing and being collected.

Quick checkScenario
In the real run above, SW4 takes far longer than the others (a genuine timeout). What happens to SW1, SW2, SW3, and SW5's results while SW4 is still pending?

Why not asyncio

A deliberate deferral, not an oversight

asyncio would require rewriting Connection to be async-native — every method, the session, the retry decorator — a much larger rewrite than this module's teaching goal justifies. ThreadPoolExecutor gets the same practical benefit (bounded concurrent network calls) with zero changes to the Connection class already built in Lesson 4. If this curriculum ever needs true high-scale async automation, that's a future module's territory, not a retrofit here.

Quick checkMyth check
Why does this lesson use ThreadPoolExecutor instead of asyncio?

Key takeaways

  • Every capstone through Phase 3.2 ran sequentially — fine at 5 devices, a real cost as fleet size grows.
  • ThreadPoolExecutor(max_workers=5) bounds concurrency deliberately; as_completed() yields results as they finish, not in submission order.
  • Real, measured result: 6.30s sequential vs 1.26s concurrent for 20 devices — a 5.01x speedup, actually executed, not assumed.
  • A genuinely slow or failing device (SW4) never blocks collecting the others' results — verified directly against the real 5-device fleet.
  • asyncio is deliberately deferred: ThreadPoolExecutor delivers the same practical benefit without rewriting Connection to be async-native.