mediumBackend EngineerCloud Infrastructure
How do goroutines differ from OS threads, and how does Go's scheduler achieve high concurrency?
Posted 18/04/2026
by Mehedy Hasan Ador
Question Details
At a cloud infrastructure company:
> "Our Go service handles 100K concurrent WebSocket connections. In Java, each thread uses ~1MB stack, so 100K threads = 100GB RAM. Go handles this in <2GB. How?"
> "Our Go service handles 100K concurrent WebSocket connections. In Java, each thread uses ~1MB stack, so 100K threads = 100GB RAM. Go handles this in <2GB. How?"
Suggested Solution
Goroutines vs OS Threads
Go's M:N Scheduler
G (Goroutines) → M (Machine/OS Thread) → P (Processor)
Multiple goroutines multiplexed onto fewer OS threads:
G1 G2 G3 G4 G5 G6 (100K goroutines)
↕ ↕ ↕ ↕ ↕ ↕
M1 M2 M3 M4 (4 OS threads = CPU cores)
↕ ↕ ↕ ↕
P1 P2 P3 P4 (4 logical processors)
Code Example
func handleConnection(conn net.Conn) {
defer conn.Close()
// Each connection gets its own goroutine (2KB stack)
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
processMessage(scanner.Text())
}
}
func main() {
listener, := net.Listen("tcp", ":8080")
for {
conn, := listener.Accept()
go handleConnection(conn) // Spawns goroutine (2KB)
}
// 100K connections = ~200MB, not 100GB
}
Why Goroutines Are So Lightweight
1. Dynamic stack: Starts at 2KB, grows/shrinks as needed (copying GC)2. Cooperative scheduling: Goroutine yields at channel ops, I/O, function calls
3. Work stealing: Idle processors steal from busy ones
// Go scheduler handles blocking automatically
resp, err := http.Get("https://api.example.com")
// Goroutine yields, OS thread serves other goroutines
// When response arrives, goroutine resumes