Skip to content
tezvyn:

Concurrent TCP server: Go goroutines vs Rust std::thread

Source: interviewMediumHow cards are made

Summary

stdlib networking and concurrency.

Key points

both accept in a loop; Go spawns a goroutine per connection (go handle(conn)); Rust spawns an OS thread (thread::spawn moving the stream).

What's really being asked

Whether you can wire up a concurrent TCP server using only standard library primitives in both languages and understand the per-connection concurrency cost.

The full answer

In Go, net.Listen("tcp", addr) returns a Listener; you loop calling ln.Accept, and for each accepted net.Conn you write go handleConn(conn). The goroutine is cheap, starts with a small growable stack, and is multiplexed by the runtime onto OS threads, so spawning one per connection scales to many thousands. In Rust, std::net::TcpListener::bind gives a listener whose incoming iterator yields each TcpStream; for every stream you call thread::spawn(move || handle(stream)), using move so the closure takes ownership of the socket across the thread boundary. Because these are real OS threads, each carries a full stack and kernel scheduling cost.

The mistakes people make

Assuming Rust std threads are as cheap as goroutines and spawning one per connection unbounded; that exhausts memory under load. Forgetting move, which fails the borrow checker. Forgetting to handle the Result from Accept or from reading the stream.

What usually comes next

How would you bound concurrency? In Go via a semaphore channel or worker pool; in Rust via a thread pool such as rayon or a custom pool, or by moving to async with Tokio. How do you shut down gracefully and drain in-flight connections?

A concrete example

Go: ln, _ := net.Listen("tcp", ":8080"); for { conn, _ := ln.Accept(); go handle(conn) }. Rust: let l = TcpListener::bind("0.0.0.0:8080")?; for s in l.incoming() { let s = s?; thread::spawn(move || handle(s)); }. Both accept-and-dispatch, but the Go version tolerates far higher connection counts because goroutines are lightweight while the Rust version pays an OS-thread price per client.

Interview question

What is the main scalability concern with spawning one std::thread per connection in a Rust TCP server compared to Go's goroutine approach?

  • a.Rust threads cannot own a moved TcpStream across the thread boundary
  • b.Rust's TcpListener cannot accept connections in a loop
  • c.std threads are OS threads with full stacks, so unbounded spawning exhausts resources unlike cheap goroutinesCorrect
  • d.Go goroutines block the listener while Rust threads do not
Why?

Each std::thread is a real OS thread with a heavy stack and scheduling cost, so one-per-connection does not scale like Go's lightweight runtime-multiplexed goroutines. Moving a stream into a thread works fine, defeating the first option.

Just read this? Test yourself on what you have been reading.

Read the original → go.dev

Put your scrolling time to good use

Learn one idea, try a quiz and save useful cards for revision. Tezvyn makes it easy to learn and stay current in your tech field, a few minutes at a time.

The iPhone app is on the way

We are building it. Until it lands, nothing here is held back from you: every interview card, your saved cards, streaks and the job board all work in Safari, plus hundreds of free practice quizzes of thirty questions each. Sign in and it all carries over to the app the day it arrives.

Want it as an icon? Tap Share at the bottom of Safari, then Add to Home Screen. It opens full screen and the cards you have read stay available offline.

Get it on Google PlayiPhone app coming soon

We are hiring for this. Open roles that interview on go — each one lists the topics its interview covers.

See open roles