I Thought Go Concurrency Was About Speed — I Was Wrong
Concurrency made my server more available, not faster.
I used to think Go concurrency was about making things faster.
Then I wrote a tiny TCP server.
What broke wasn’t performance — it was availability.
A single blocking Accept() call meant:
one client connected
everyone else waited
Adding a goroutine didn’t make the server faster.
It made the server usable.
That’s when concurrency stopped feeling like a language feature and started feeling like a design decision.
I wrote about this realization while evolving a raw TCP server in Go 👇
I used to think Go concurrency was about goroutines and channels.
You know the advice:
“Just put it in a goroutine.”
But none of that really clicked for me until I wrote a very small, very boring TCP server — and watched it behave in ways I didn’t expect.
This post is about how I slowly evolved a raw TCP server in Go and discovered what concurrency actually means — not as a concept, but as behavior.
Stage 1: The simplest possible TCP server
I started with the smallest thing that could work:
listen on a TCP port
accept one connection
print who connected
Output
What this taught me
Once a client connected, the server stopped making progress.
No new connections. No errors. Just… stuck.
That’s when it hit me:
Accept()is a blocking call.
The server wasn’t idle — it was blocked.
Concurrency hadn’t entered the picture yet, but the problem already existed.
Stage 2: Reading input — still single-client
Next, I tried reading from the connection.
Output
What this taught me
The server now did something useful.
But it still handled only one client.
If a second client tried to connect while the first was active, it had to wait.
The server was alive — but unavailable.
That distinction mattered more than I expected.
Stage 3: Accepting multiple connections… serially
My next instinct was obvious:
for {
conn, _ := lis.Accept()
handle(conn)
}Surely this meant multiple clients, right?
Output
What this taught me
Even with a loop, clients were handled one after another.
The server could accept multiple connections — just not at the same time.
This is when I wrote this line in my notes:
Accepting connections is easy.
Handling them concurrently is the real problem.
That sentence stayed with me.
Stage 4: Goroutine per connection — the first breakthrough
Then came the smallest change that changed everything:
for {
conn, _ := lis.Accept()
go handleConn(conn)
}Output
Messages from different clients started interleaving naturally.
What this taught me
This wasn’t about speed.
It was about availability.
Each connection now had its own execution path.
No client blocked another.
For the first time, goroutines stopped feeling like magic and started feeling necessary.
What this taught me about concurrency
Before writing this TCP server, I thought concurrency in Go was mainly about:
doing more work in parallel
improving throughput
“making things faster”
This exercise completely changed that.
The biggest improvement didn’t come from optimization.
It came from availability.
Before goroutines:
one blocking
Accept()one blocking read
one client could make the entire server unavailable
The server wasn’t broken.
It was just unreachable.
Adding a goroutine per connection didn’t make the server faster.
It made the server usable.
Multiple clients could connect.
One slow or silent client no longer blocked everyone else.
That’s when concurrency stopped feeling like a language feature and started feeling like a design decision.
The mental shift that finally clicked for me
I used to ask:
“How do I make this code concurrent?”
Now I ask:
“What is blocking, and who does it make unavailable?”
Once I started thinking in those terms:
goroutines felt obvious
concurrency felt justified
the design choices made sense
Go didn’t “add concurrency” to my server.
It removed unnecessary unavailability.
That distinction changed how I look at backend systems.
Where this goes next
This server is concurrent — but still incomplete.
It doesn’t coordinate clients.
It doesn’t handle slow consumers.
It doesn’t shut down gracefully.
In the next part, I want to explore what happens after availability:
how concurrent goroutines coordinate
why channels change the design
and how ownership keeps systems sane
But this first realization deserved its own space.
If this post saved you even one hour of confusion, it did its job.









