This page explains buffered channels in Go: how make(chan T, n) differs from an unbuffered channel, exactly when sends and receives block, how len and cap relate to capacity, and why buffering is backpressure rather than an unlimited queue. For the full channel tour (range, close, select), read channels in Go; for workers and fan-out, see goroutines and multiple receivers on one channel.
Tested with Go 1.24 on Linux.
Quick answer: what “buffered” means
A buffered channel has a fixed capacity n from make(chan T, n). Values sit in a queue until a receiver takes them: a send blocks only when the buffer is already full, and a receive blocks only when the buffer is empty. An unbuffered channel has capacity zero and synchronizes each send with a matching receive.
What is a buffered channel?
A channel carries typed values between goroutines. With capacity greater than zero, the runtime stores up to n values between sender and receiver so the two sides can proceed at different speeds until the queue fills or drains. That decouples producer and consumer up to n—it does not remove the need for a receiver forever.
Syntax: make(chan T, n), len, and cap
package main
import "fmt"
func main() {
unbuf := make(chan int)
buf := make(chan int, 3)
fmt.Println(cap(unbuf), cap(buf))
}You should see 0 and 3. cap(ch) is always the buffer size. For an unbuffered channel, cap is zero.
package main
import "fmt"
func main() {
ch := make(chan int, 3)
fmt.Println("empty", len(ch), cap(ch))
ch <- 10
ch <- 20
fmt.Println("two items", len(ch), cap(ch))
<-ch
fmt.Println("after one recv", len(ch), cap(ch))
}You should see empty 0 3, then two items 2 3, then after one recv 1 3. len(ch) is how many values are queued right now; it can change the instant another goroutine runs, so do not use len for cross-goroutine synchronization.
Send and receive blocking rules
| Operation | Unbuffered (make(chan T)) |
Buffered (make(chan T, n), n > 0) |
|---|---|---|
| Send | Blocks until another goroutine receives | Blocks only when len == cap (buffer full) |
| Receive | Blocks until another goroutine sends | Blocks only when len == 0 (buffer empty) |
The buffer is not an infinite queue: if the producer keeps sending faster than the consumer receives, the buffer fills and the producer blocks—that is backpressure.
Buffered vs unbuffered channels
Unbuffered channels act like a handoff: the sender and receiver meet at the same time. Buffered channels let the sender deposit values into the queue (up to n) before a receiver is ready, and let the receiver drain multiple values that arrived earlier.
package main
import "fmt"
func main() {
ch := make(chan string)
go func() { ch <- "ping" }()
fmt.Println(<-ch)
}You should see ping (unbuffered rendezvous).
package main
import "fmt"
func main() {
ch := make(chan string, 2)
ch <- "a"
ch <- "b"
fmt.Println(<-ch, <-ch)
}You should see a b (two sends completed before receives, because cap is 2).
Capacity, backpressure, and producer–consumer behavior
Small buffers smooth short bursts: the producer can get a few values ahead of the consumer. A large buffer delays visible blocking but stores more in-flight work and can hide a slow consumer until memory and latency grow. Choose capacity from expected burst size, a concurrency limit (similar to a semaphore), or measured behavior—not “as large as possible” by default.
Producer, consumer, close, and range
Only the sender side should close a channel when no more values will be sent. Receivers use for v := range ch to drain until close.
package main
import (
"fmt"
"sync"
)
func main() {
ch := make(chan int, 2)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for i := 1; i <= 5; i++ {
ch <- i
fmt.Println("sent", i)
}
close(ch)
}()
for v := range ch {
fmt.Println("recv", v)
}
wg.Wait()
}You should see interleaved sent and recv lines; with buffer 2 the producer can usually send two values before it may block waiting for space.
Real-world uses (brief)
| Pattern | Role of buffering |
|---|---|
| Worker pool / job queue | Bounded queue between producer and fixed workers |
| Rate limiting / semaphore | Token buffer caps concurrent work |
| Bursty logging or metrics | Short queue absorbs spikes without blocking callers |
| Pipeline stages | Small buffer between stages reduces stalls |
Deadlocks and common mistakes
Sending on a full buffer with no receiver and no other goroutine to free space deadlocks single-threaded programs. Receiving from an empty buffer with no sender deadlocks the same way. A huge buffer does not fix a missing consumer—it only postpones blocking and uses more memory.
Relying on len(ch) to decide “safe to send” is a race unless you already hold synchronization elsewhere. Assuming close is optional for every receive path is wrong: range over an open channel never ends if nobody closes.
Choosing buffer size
Start small (0, 1, or a few times your natural parallelism). Match a known limit (for example “at most three downstream RPCs in flight”). Measure under load and adjust; if latency grows with buffer size, the consumer is slower than the producer and buffering only queues work.
Cheat sheet
| Concept | Meaning |
|---|---|
make(chan T) |
Unbuffered channel |
make(chan T, n) with n > 0 |
Buffered channel, capacity n |
cap(ch) |
Maximum queued values |
len(ch) |
Values currently queued (not for sync) |
| Send to full buffer | Blocks |
| Receive from empty buffer | Blocks |
close(ch) |
No more sends; range ends after drain |
| Large buffer | More queueing, more memory, can hide slowness |
| Small buffer | Tighter backpressure, less smoothing |
Summary
A golang buffered channel uses make(chan T, n) with n > 0 so values queue until the buffer fills; sends block when len == cap, receives block when len == 0. Unbuffered channels synchronize each send with a receive. Use cap and len for inspection only; size buffers for real bursts and backpressure, not as unlimited queues. Close from the sender; pair close with range or explicit receives to avoid leaks and deadlocks.

