Part 8 · 1 chapters · ~8 min

One Problem in Five Languages

A concurrent, rate-limited fetcher (at most 10 requests in flight, a per-request timeout, results collected, cancellation on first fatal error) written in Node, Go, Rust, Java and Python, compared for correctness, ergonomics and the concurrency model each language encourages.

13

The same fetcher, five ways

code
// Node: a small pool of async workers pulling from a shared index
async function fetchAll(urls, limit = 10) {
  const out = new Array(urls.length); let i = 0;
  const worker = async () => { while (i < urls.length) { const k = i++; out[k] = await fetch(urls[k], { signal: AbortSignal.timeout(5000) }).then(r => r.status); } };
  await Promise.all(Array.from({ length: limit }, worker));
  return out;
}

// Go: errgroup with a limit and a context that cancels on the first error
g, ctx := errgroup.WithContext(ctx); g.SetLimit(10)
for i, u := range urls { g.Go(func() error { s, err := get(ctx, u, 5*time.Second); out[i] = s; return err }) }
err := g.Wait()

// Rust (Tokio): buffer_unordered caps concurrency at 10
let out: Vec<_> = stream::iter(urls).map(|u| async move { timeout(Duration::from_secs(5), client.get(u).send()).await })
    .buffer_unordered(10).collect().await;

// Java 21: virtual threads + a semaphore
try (var ex = Executors.newVirtualThreadPerTaskExecutor()) {
  var sem = new Semaphore(10);
  var fs = urls.stream().map(u -> ex.submit(() -> { sem.acquire(); try { return get(u); } finally { sem.release(); } })).toList();
  for (var f : fs) results.add(f.get());
}

# Python asyncio: a semaphore and gather
async def fetch_all(urls, limit=10):
    sem = asyncio.Semaphore(limit)
    async def one(u):
        async with sem: return await asyncio.wait_for(get(u), 5)
    return await asyncio.gather(*(one(u) for u in urls))
languagemodel encouragednotable
Nodesingle-threaded asyncno data races, but await interleavings (part 0)
Gogoroutines + context + errgroupcancellation by convention through context
Rustasync futures, checked at compile timedata races impossible in safe code; Send/Sync bounds
Javavirtual threads (blocking style, cheap threads)no function colouring; structured scopes emerging
Pythonasyncio for IO; processes for CPUthe GIL (and free-threaded builds from 3.13) matter for CPU work