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?"
Suggested Solution
Goroutines vs OS Threads
| Aspect | OS Thread | Goroutine |
|---|---|---|
| Stack size | 1-8 MB (fixed) | 2KB (grows dynamically) |
| Creation cost | ~1ms | ~0.3μs (1000x cheaper) |
| Context switch | ~1-10μs (OS) | ~0.2μs (Go runtime) |
| Managed by | OS kernel | Go runtime scheduler |
| 100K instances | 100GB+ RAM | ~200MB RAM |
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
- Dynamic stack: Starts at 2KB, grows/shrinks as needed (copying GC)
- Cooperative scheduling: Goroutine yields at channel ops, I/O, function calls
- 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