Buffered Channels
Buffered channels have a capacity — sends succeed without blocking until the buffer fills.
Introduction
Buffered channels have a capacity — sends succeed without blocking until the buffer fills. They implement queues, semaphores, and backpressure in high-throughput systems. Choosing the right buffer size affects latency, memory, and deadlock risk.
Understanding when buffer size 0 (sync), 1 (handoff), or N (pipeline) is appropriate separates production-ready concurrent code from tutorial examples. This lesson covers semaphore patterns, rate limiting, and avoiding buffer-related deadlocks.
Buffered channels appear in worker pools, logging pipelines, and Kubernetes controller work queues — patterns you will implement in the final project.
The story
A Cloudflare log ingestion worker uses a buffered channel of capacity 1000 to decouple HTTP handlers from slow S3 uploads. Handlers enqueue log batches without blocking until the buffer fills — then backpressure signals the load balancer to shed traffic before the process OOMs.
Choosing buffer size is an engineering trade-off: too small adds latency under burst; too large hides backpressure until memory exhaustion — production tuning uses metrics on channel length via custom instrumentation.
Understanding the topic
Key concepts
- make(chan T, n) creates buffer of n elements.
- len(ch) current items; cap(ch) buffer capacity.
- Send non-blocking when len < cap; blocks when full.
- Receive non-blocking when len > 0; blocks when empty.
- Semaphore: chan struct{} with cap N limits concurrency.
- Too large buffer hides backpressure until OOM.
Step-by-step explanation
- Producer sends until buffer full, then blocks.
- Consumer receives, freeing slot for blocked sender.
- Zero buffer: synchronous handoff.
- Size-1 buffer: async handoff with one in-flight item.
- Close drains remaining buffer then signals done.
- Select with default enables non-blocking try-send/receive.
Practical code example
Semaphore limiting concurrent API calls with buffered channel:
package mainimport ("fmt""sync""time")func fetch(id int, sem chan struct{}, wg *sync.WaitGroup) {defer wg.Done()sem <- struct{}{} // acquiredefer func() { <-sem }() // releasefmt.Printf("fetch %d started\n", id)time.Sleep(100 * time.Millisecond)fmt.Printf("fetch %d done\n", id)}func main() {const maxConcurrent = 2sem := make(chan struct{}, maxConcurrent)var wg sync.WaitGroupfor i := 1; i <= 5; i++ {wg.Add(1)go fetch(i, sem, &wg)}wg.Wait()}
Line-by-line code explanation
ch := make(chan LogBatch, 1000)creates a buffered channel with capacity 1000.ch <- batchsends without blocking while buffer has space.len(ch)returns current queued items — useful for metrics/alerts.cap(ch)returns the buffer capacity (1000 in this example).blocking send when full— producers wait when buffer is at capacity, applying backpressure.worker pool pattern— N workers drain a buffered job channel concurrently.time.Afterwithselectadds timeout when buffer stays full too long.prefer unbuffered for synchronization— add buffering only when profiling shows benefit.
Key takeaway: Buffered channel as counting semaphore. maxConcurrent=2 limits parallel fetches. Adjust buffer to match resource limits (DB connections, API rate).
Real-world use
Where you'll use this in production
- Limiting concurrent database queries to pool size.
- Job queue between API handler and background workers.
- Batched log shipping with buffer before flush.
- Token bucket rate limiters for external API calls.
Best practices
- Size buffer based on measured producer/consumer rates.
- Monitor channel len in metrics for queue depth alerting.
- Use semaphore pattern for resource-bound concurrency.
- Prefer small buffers — large buffers mask overload.
- Combine with context timeout on blocked sends.
Common mistakes
- Huge buffer causing memory bloat under load spike.
- Deadlock: all goroutines blocked on full channel, none receiving.
- Assuming send always succeeds — blocks when full.
- Using buffer to 'fix' race without proper sync.
- Not handling shutdown when buffer has pending items.
Advanced interview questions
Q1Beginnerlen vs cap on channel?
Q2BeginnerSemaphore with channel?
Q3IntermediateChoose buffer size?
Q4IntermediateNon-blocking channel operation?
Q5AdvancedDesign backpressure for slow consumer.
Summary
Buffered channels queue up to cap elements before blocking. Use as semaphores to limit concurrent resource usage. Size buffers deliberately — oversized buffers hide overload. Monitor queue depth in production systems. Next lesson: select — multiplexing channel operations.