Cloud Explainers

Why Golang Uses Goroutines Instead of Threads

Go (Golang)9 min videoPublished Nov 7, 2026

Go starts a goroutine for every task where other languages warn you about too many threads. We measured why on one machine: real OS threads in C against goroutines in Go, for memory, start time, stack size, switching, and the one case where Go starts a real thread anyway.

Why Golang Uses Goroutines Instead of Threads, the full picture: diagram of Goroutines, Go runtime, Network poller, OS threads, Linux kernel, CPU cores, Inside Ana's Go program, The operating system
The whole picture, from the video's recap.

Ten thousand customers

You've read that a Go server starts a goroutine for every customer, while other languages warn you never to start thousands of threads. So how can Go get away with it? Goroutines run on threads in the end. What's actually different?

One task per waiting customer: diagram of Customer, Ana's server, Thousands of threads?, A goroutine each

People have asked this for years. On the golang-nuts mailing list, back in 2013, someone asked how Go can run a hundred thousand goroutines on a single CPU, switching between all of them without large overhead, when threads each need their own fixed stack. Today we measure all of it.

A real question, golang-nuts mailing list, 2013: diagram of golang-nuts, 2013, 100k+ goroutines, Switching them all, Threads

Ana runs a tea shop, and its order server waits on lots of customers at once. On one machine, we'll start ten thousand real OS threads, then ten thousand goroutines, and compare memory, start time and switching. And one question to keep in mind: when does Go quietly start a real thread for a goroutine anyway? We'll catch it doing that.

What a thread costs

So what is a thread? It's the operating system's unit of work. A program asks the kernel, the core of Linux, for one, and the kernel decides when each thread gets a CPU core. Each thread also needs a stack, memory for its function calls, reserved in one piece up front, plus a small stack inside the kernel.

A thread belongs to the kernel: diagram of Your program, Threads, Linux kernel, CPU cores, Every thread: a stack reserved up front, plus one in the kernel

Here's a small program in C, using plain OS threads: one thread per waiting customer. Ten thousand of them, plus the program's own main thread, started in 105 milliseconds. 82 megabytes of memory, 156 more inside the kernel, and 78 gigabytes of address space reserved for their stacks: a promise of room, not memory in use.

laptop: Ana's terminal
c/threads 10000threads started:   10000
OS threads:        10001
time to start:     0.105 s
memory used:       82 MB
kernel stacks:     156 MB
stack reserved:    78 GB

Reserved isn't used: that's eight megabytes of address space per thread, set aside in case it's needed. What each thread really costs is its memory plus its kernel stack: about 24 kilobytes, for a thread that does nothing but wait.

One waiting thread: reserved vs really used: chart of Stack room reserved: 8192 KB, Really used: 24 KB, About 24 KB each, doing nothing

Now ten times as many: a hundred thousand threads. Guess first: what happens? Linux refuses. Thread number 63,462 failed: resource temporarily unavailable, which means the system has no room for one more. This machine allows 63,759 threads in total, a limit Linux sets from its 8 gigabytes of memory, and every thread already running counts too. And remember, these threads did nothing at all but wait.

laptop: Ana's terminal
c/threads 100000thread 63462 failed: Resource temporarily unavailable
threads started:   63461
OS threads:        63462
time to start:     0.645 s
memory used:       515 MB
kernel stacks:     991 MB
stack reserved:    499 GB
laptop: Ana's terminal
cat /proc/sys/kernel/threads-max63759

What a goroutine is

A goroutine is different: the kernel never sees it. The Go runtime, code that comes inside every Go program, keeps its own list of goroutines, and runs them on a few OS threads, about one per CPU core. When a goroutine has to wait, the runtime puts another one on that thread.

In code, the difference is small. In C, each customer gets a call that asks the kernel for a thread, and the thread waits on a condition: a signal that the shop is open. In Go, you write the word go in front of a function call, and that function runs as a goroutine, waiting on a channel.

One waiting customer, two ways: cards of C: start a thread, C: it waits, Go: start a goroutine, Go: it waits

The same job in Go: ten thousand goroutines, one per customer. Started in 5 milliseconds, about 21 times faster. 32 megabytes, nothing in the kernel, and the whole program uses just 11 OS threads, for ten thousand goroutines.

laptop: Ana's terminal
go run ./goroutines 10000goroutines started: 10000
OS threads:         11
time to start:      0.005 s
memory used:        32 MB
kernel stacks:      0 MB

A hundred thousand: fine. A million goroutines: started in 697 milliseconds, still on 11 threads. A hundred times more goroutines, and not one extra thread. That's far past where threads ran out on this same machine.

laptop: Ana's terminal
go run ./goroutines 100000goroutines started: 100000
OS threads:         11
time to start:      0.052 s
memory used:        271 MB
kernel stacks:      0 MB
laptop: Ana's terminal
go run ./goroutines 1000000goroutines started: 1000000
OS threads:         11
time to start:      0.697 s
memory used:        2620 MB
kernel stacks:      0 MB

Per task, that million used about 2.7 kilobytes each. A waiting thread cost about 24. So a goroutine is roughly 9 times smaller here, and it needs nothing from the kernel's thread limit.

Real memory per waiting task, measured: chart of Goroutine: 2.7 KB, OS thread: 24 KB, About 9x smaller, and no thread limit

That's why real Go servers lean on them. Go's own web server, net/http, starts a new goroutine for each connection. And at Uber, engineers wrote in 2022 that, across hundreds of thousands of running services, their Go services showed about eight times more concurrency than their Java ones.

Goroutines in production: cards of net/http, From Go's docs, Uber, 2022, ~8x more concurrency

Stacks that grow

Why is a goroutine so small? Mostly its stack. A thread's stack can't grow, so Linux reserves room for the worst case: eight megabytes here. A goroutine starts with a few kilobytes. When a function needs more, the runtime copies the stack to a bigger block and carries on, up to a gigabyte.

Two kinds of stack: comparison table of OS thread vs Goroutine, Can it grow?, At the start, Needs more?, Largest

Let's push it. A function that calls itself a million times, each call taking a little room, at least sixty-four bytes. That's sixty-four megabytes of stack or more. ulimit dash s shows a thread's stack size, in kilobytes: eight megabytes. On a C thread, with that fixed eight megabytes: segmentation fault, the program ran off the end of its stack and crashed. Now the same calls in a goroutine, which started with a few kilobytes. Will it crash too? It finishes: a million calls deep. The runtime kept moving its stack to bigger blocks, and the program never noticed.

laptop: Ana's terminal
ulimit -s8192
laptop: Ana's terminal
c/growSegmentation fault
laptop: Ana's terminal
go run ./growcalls deep: 1000000

Switching for less

Memory is half the story; switching is the other half. When one thread has to wait, the CPU leaves your program and enters the kernel. The kernel saves that thread's state, picks the next thread, and switches the CPU over to it. Every switch is a trip into the kernel and back.

A thread switch goes through the kernel: diagram of Thread A, Kernel, Thread B, I have to wait, saved, run B, Every switch: into the kernel and back

A goroutine switch never leaves your program. When a goroutine waits on a channel, the Go runtime saves a few values, takes the next goroutine from its list, and keeps the same thread running. And if a goroutine computes for too long, the runtime interrupts it, so the others still get a turn.

A goroutine switch stays inside your program: diagram of Goroutine A, Go runtime, Goroutine B, waiting, saved, your turn, time's up, No trip into the kernel

Here's the cost. Two threads passing a token back and forth, two hundred thousand times each way: 6,620 nanoseconds per hand-off. Two goroutines doing the same over a channel: 66. About 100 times faster.

laptop: Ana's terminal
c/switchthreads, one hand-off:    6620 ns
laptop: Ana's terminal
go run ./switchgoroutines, one hand-off: 66 ns

That's what makes a goroutine per customer practical: when thousands of them wake up and wait all day, each switch costs a few dozen nanoseconds, not thousands.

One hand-off between two tasks, measured: chart of OS threads: 6620 ns, Goroutines: 66 ns, About 100x faster

When Go needs a thread

Now the question from the start. A system call is your program asking the kernel to do something, like reading a file. A hundred goroutines that sleep: still 5 threads. A hundred more stuck in a blocking system call, a read the kernel holds: 103 threads. Each stuck goroutine took a real thread with it, so the runtime started new ones to keep the others running.

laptop: Ana's terminal
go run ./blocking100 sleeping goroutines, OS threads: 5
100 more stuck in a system call, OS threads: 103

So that's when Go starts a real thread for a goroutine: when the kernel holds it in a system call. Waiting on the network is different. Those goroutines hand their connection to the network poller, a part of the runtime that watches every network connection at once, so ten thousand customers don't need ten thousand threads.

What it means for you

Side by side: the kernel runs threads, the Go runtime runs goroutines. Fixed stack against a growing one. 24 kilobytes against 2.7. 6,620 nanoseconds per switch against 66. And a ceiling of 63,461 threads, against a million goroutines and counting. On the same machine, goroutines won every measurement we ran.

OS thread vs goroutine, on one machine: comparison table of OS thread vs Goroutine, Run by, Stack room, Memory each, One switch, Most we ran

Goroutines still have limits. Real work runs on threads, so heavy computing is capped by your CPU cores; GOMAXPROCS, a Go setting that defaults to your number of cores, sets how many threads run Go code at once. A million goroutines still took 2.6 gigabytes. Blocking system calls, and C code called from Go, take real threads. And a goroutine that never finishes is a leak.

What goroutines don't fix: cards of CPU-heavy work, Memory adds up, System calls, C code, Leaks

The idea has spread. Java 21 added virtual threads: the Java runtime schedules them on a few OS threads, much like Go does with goroutines. Go simply had it from the start, so every Go library is written for it.

Same idea, elsewhere: comparison table of Go vs Java, Called, Run on, Since

Every command and the lab files, the C threads and the Go versions, are linked in the description, so you can measure your own machine.

Run it yourself: cards of c/threads.c, goroutines/, switch, grow, blocking/

Recap

So: threads belong to the kernel and reserve a big stack each. Goroutines live inside your program, start tiny, grow when needed, and share a few threads, until one gets stuck in the kernel.

If this made goroutines click, a like helps YouTube show it to more beginners, and subscribing brings you the next Go video.

Every number here came from one real machine. Thanks for watching Cloud Explainers.

Sources

Demos: one container on our own Mac standing in for Ana's laptop; the tea shop is made up. Watch the full video on YouTube.

More videos

Linux9 min

Linux Makefiles Will Finally Make Sense After This

Auth (OAuth, JWT)9 min

SAML vs OAuth vs OpenID Connect: What's the Difference?

Python7 min

How to Fix Python cron Jobs That Never Run

Linux22 min

Linux Bash Scripting: Full Tutorial for Beginners

Networking10 min

How PXE Network Boot Works

Active Directory9 min

How Active Directory Works

Linux10 min

How to Fix a Linux systemd Service That Won't Start

Linux9 min

Linux Logs: syslog vs journald

Auth (OAuth, JWT)9 min

JWT vs Session Cookies: Which One Should You Use?

Docker10 min

Docker vs Docker Compose: Which One Should You Use?

Docker9 min

How to Fix Docker Compose Containers That Can't Talk to Each Other

Django (Python web framework)22 min

Django Full Tutorial for Beginners

Linux9 min

How Linux systemd Works

Networking8 min

How to Fix 'Windows Cannot Access' Network Share Errors (SMB)

APIs10 min

REST vs gRPC: Which One Should You Use?

Certificates & PKI (ADCS)9 min

How to Fix SSL Certificate Chain Errors (Missing Intermediate)

Python9 min

Every Python Concept for Beginners in 10 Minutes

Databases & SQL9 min

Every SQL Join Type in 10 Minutes

OpenRouter (one API for many AI models)9 min

Why OpenRouter Free Models Hit Their Limit So Fast

Gemini API & Google AI Studio9 min

Why Your Gemini API Free Tier Runs Out So Fast

OpenRouter (one API for many AI models)10 min

OpenRouter Will Finally Make Sense After This

Caching & Redis10 min

How Redis Caching Works

Gemini API & Google AI Studio10 min

Gemini API vs Vertex AI: Which One Should You Use?

Gemini API & Google AI Studio8 min

How to Fix a Leaked Gemini API Key

Gemini API & Google AI Studio9 min

How to Fix Gemini API Error 429: Resource Exhausted

Claude & the Claude Developer Platform10 min

Claude Token Pricing Will Finally Make Sense After This

Python11 min

Why Python Web Scraping Returns Nothing (and the Fix)

Python10 min

Python uv vs pip: Which One Should You Use?

Python22 min

Python Automation: Full Tutorial for Beginners

Auth (OAuth, JWT)9 min

How OAuth Login Works

Gemini API & Google AI Studio10 min

Google AI Studio Will Finally Make Sense After This

Docker10 min

What Happens When You Run a Docker Container

Claude & the Claude Developer Platform8 min

How to Fix Claude Code Billing Your API Key Instead of Your Plan

Auth (OAuth, JWT)9 min

Why Passkeys Can't Be Phished Like Passwords

PowerShell10 min

PowerShell vs Bash: Which One Should You Learn?

Linux10 min

The Linux Roadmap for Beginners

Django (Python web framework)9 min

Django vs FastAPI: Which One Should You Use?

Databases & SQL10 min

Why SQLite Is the Most Used Database in the World

Databases & SQL11 min

How a NoSQL Database Like MongoDB Works

Python11 min

How to Fix Python pip error: externally-managed-environment

Databases & SQL9 min

How to Fix MySQL Too Many Connections Database Errors

Go (Golang)9 min

Golang Concurrency Will Finally Make Sense After This

AWS10 min

How Amazon S3 Works

Kubernetes10 min

How Kubernetes Works

Git10 min

How Git Works

Docker10 min

How to Fix Docker Port Already Allocated Errors

PostgreSQL10 min

How PostgreSQL Works

Git8 min

How to Fix Git Merge Conflicts

Linux9 min

How to Fix No Space Left on Device in Linux

Networking10 min

How the Internet Works

Databases & SQL9 min

SQL vs NoSQL: When Each One Wins

Auth (OAuth, JWT)8 min

How to Fix JWT Token Expired Errors (401 Unauthorized)

Claude & the Claude Developer Platform9 min

Claude API vs Claude Pro: Why Your Subscription Doesn't Include It

AI agent skills9 min

Why AI Agent Skills Beat Giant Prompts

Linux10 min

Linux Cron Jobs Will Finally Make Sense After This

Cybersecurity10 min

Security Logs Will Finally Make Sense After This

Cybersecurity8 min

How to Fix a Security Leak: An API Key Pushed to GitHub

DNS9 min

DNS Will Finally Make Sense After This

Cybersecurity9 min

Never Ignore a Security Update

Auth (OAuth, JWT)9 min

Two-Factor Authentication Will Finally Make Sense

Networking8 min

Never Expose a Database to the Internet

Cybersecurity10 min

SSH Keys vs Passwords: Which Is Safer for Security?

Linux10 min

The 5 Levels of Linux Boot: From Power On to Login

Networking12 min

The 5 Levels of Network Firewalls

Cybersecurity10 min

Password Security: Why Hackers Crack Weak Passwords

Linux9 min

Why Your Linux Server Runs Out of Memory

Networking10 min

HTTP vs HTTPS: What Actually Changes on Your Network

Linux12 min

Every Linux File Permission You Need to Know

Linux10 min

Linux Processes vs Threads: When Each One Wins

Networking11 min

The OSI Model Will Finally Make Sense After This

Git11 min

The 7 Levels of Git

Networking10 min

Every Network Troubleshooting Command You Need

Cybersecurity10 min

Hashing vs Encryption in Cybersecurity: When Each One Wins

Networking11 min

Why the Internet Ran Out of IP Addresses

Linux11 min

Linux Package Managers: apt vs Snap vs Flatpak

Python9 min

Why Python Is Slow (and When It Doesn't Matter)

Networking11 min

TCP vs UDP: When Each One Wins

Linux8 min

Stop Running Linux as Root (Do This Instead)

Linux11 min

Linux Users and Groups Will Finally Make Sense

Cybersecurity9 min

Why Phishing Beats Good Security (and How to Spot It)

Docker12 min

The 7 Levels of Docker

AI agents & MCP12 min

AI Agents Will Finally Make Sense After This

Git8 min

Stop Using git push --force (Use This Instead)

AI agent skills10 min

Agent Skills vs MCP: When Each One Wins

Networking12 min

Networking Will Finally Make Sense After This

Linux12 min

Every Linux Command You Actually Need

Cybersecurity12 min

The 7 Levels of Cybersecurity

System design12 min

Every System Design Concept You Need, in One Diagram

PostgreSQL10 min

Why PostgreSQL Ignores Your Index