---
title: 'Buffered and Unbuffered Channels'
source: 'https://academia.sh/en/courses/go-concurrency/buffered-and-unbuffered-channels'
course: Concurrency
language: en
updated: '2026-09-14T10:59:32+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Shows that a channel's capacity determines where blocking starts, by measuring how many attempts pass without blocking at three capacities"
    - 'Proves, by running it, that a receiver taking a value on a buffered channel reopens the slot that had closed'
    - 'Shows that the buffer does not remove synchronization, only defers it, making it impossible for the sender to know the moment of receipt'
---

# Buffered and Unbuffered Channels

A channel's capacity changes where blocking starts: a send passes without waiting at all until capacity fills, and once it fills, it blocks exactly like an unbuffered channel. How many of five attempted sends pass without blocking is measured at three capacities. Bounding measurement: the buffer does not remove synchronization, it only defers it — once a send to a buffered channel completes, the sender still does not know whether the receiver has taken that value.

The previous lesson measured that an unbuffered channel does two jobs at once: carrying a
value and making the sender and the receiver meet at the same point. Adding capacity to a
channel does not remove these two jobs — it only loosens the bond between them. This lesson
measures **where** the buffer changes blocking behavior, and bounds what it does not change
in a separate section.

Giving a channel capacity turns it into a structure that holds a small array inside itself.
The existence of this array gives the sending side a kind of flexibility: even if the
receiver is not yet ready, a send can proceed without waiting as long as the buffer still
carries an empty slot. But this flexibility has a limit, and that limit is exactly the
capacity itself. This lesson measures that limit and where it gets crossed.

Capacity is fixed once, when the channel is created, and cannot be grown or shrunk
afterward — a channel opened with `make(chan int, 3)` can hold at most three values for its
entire lifetime. This fixedness shows that capacity is not a runtime setting but a design
decision: the person writing the code decides from the start how far the sender is allowed
to "get ahead of" the receiver. There is no single rule for the right size of this decision
— too small a capacity makes the sender wait more often than necessary, too large a capacity
hides how far behind the receiver has fallen; this lesson measures not how the size should
be chosen, but what a chosen size does.

## Capacity Is Where Blocking Starts

The second argument in the call `make(chan T, n)` is the channel's capacity; if it is left
out (`make(chan T)`), the capacity is zero, and the previous lesson's unbuffered channel is
what results. An unbuffered channel is not a separate type — it is a channel whose capacity
is zero, and a zero capacity produces the same result as "the buffer is always full": every
send waits directly for a receiver, exactly as on a channel whose capacity is already full.

This single difference is what lets both channel uses be written with the same syntax: when
a function takes a `chan int` parameter, there is no need to tell from inside the code
whether the caller passed a buffered or an unbuffered channel — both use the same send and
receive syntax, the only difference is when they wait.

On a channel with capacity `n`, the first `n` sends complete immediately, even with no
receiver at all; the channel holds these values inside itself. The `n+1`th send, though,
means the buffer is now full, and it waits for a receiver to arrive, exactly as on an
unbuffered channel. This limit is not carried in the channel's own type but in the second
argument given to the `make` call; the same `chan int` type can be created with capacity `0`
or with capacity `1000`, and both uses look identical in the source code — the difference is
only in the `make` line.

This can be measured at three separate capacities, with five attempted sends each time,
using `select`+`default` — without starting a single goroutine, because having no receiver
at all is part of what is being measured.

```go
// capacity.go — a send goes through unblocked until the buffer fills, once full an attempt does not pass
package main

import "fmt"

func howManyGoThrough(capacity, attempts int) int {
	channel := make(chan int, capacity)
	passed := 0
	for i := 0; i < attempts; i++ {
		select {
		case channel <- i:
			passed++
		default:
		}
	}
	return passed
}

func main() {
	for _, capacity := range []int{0, 2, 4} {
		fmt.Printf("capacity=%d attempts=5 passed=%d\n", capacity, howManyGoThrough(capacity, 5))
	}
}
```

```
capacity=0 attempts=5 passed=0
capacity=2 attempts=5 passed=2
capacity=4 attempts=5 passed=4
```

**GC9.** — No receiver was ever started; the result is therefore strictly deterministic and
depends on no random timing — there is no goroutine here at all, so what is measured is not
timing but the channel's own capacity rule. With capacity `0`, no send passes — the previous
lesson's "an attempt never passes when there is no receiver at all" showing up again here.
With capacity `2`, exactly two pass, and with capacity `4`, exactly four: the `passed` count
is, in every case, **exactly equal to the capacity**, never reaching the fifth attempt at
all. Fifty attempts instead of five would not change the result — every attempt after the
buffer fills falls into `default` the same way, until a receiver arrives.

This shows exactly what the buffer does: it does not remove blocking, it defers the point
where blocking starts **by the size of the capacity**. At zero capacity there is no deferral
at all; as capacity grows, the sending side also gains a growing number of sends it can make
with no receiver at all.

This deferral is the size of the window in which the sender can operate **independently** of
the receiver. At capacity `0` there is no window at all, the sender has to wait for the
receiver on every step. As capacity grows, the sender can take that many steps in a row
without looking at the receiver's speed — but once the window closes (from the fifth attempt
onward in the `howManyGoThrough` measurement), behavior goes back to being identical to an
unbuffered channel's.

## len and cap Show the Buffer's Instantaneous State

A channel's current fullness can also be read from inside the program: the built-in `len`
function returns the number of values waiting in the buffer, not yet received; the `cap`
function returns the channel's fixed capacity. The two carry independent information — `cap`
never changes from the moment the channel is created, `len` changes on every send and
receive.

```go
// lencap.go — len gives the count of values currently waiting in the channel's buffer, cap gives the capacity
package main

import "fmt"

func main() {
	channel := make(chan int, 3)
	fmt.Println("empty       : len=", len(channel), "cap=", cap(channel))

	channel <- 1
	channel <- 2
	fmt.Println("two filled  : len=", len(channel), "cap=", cap(channel))

	<-channel
	fmt.Println("one received: len=", len(channel), "cap=", cap(channel))
}
```

```
empty       : len= 0 cap= 3
two filled  : len= 2 cap= 3
one received: len= 1 cap= 3
```

**GC10.** — While the channel is empty, `len` is zero; after two sends, it is two; after one
receive, it is one. `cap` keeps holding three the whole time, unchanged, because the capacity
is fixed at the moment of `make` and cannot be changed afterward. When `len(channel) ==
cap(channel)`, the channel is full, and the next send (exactly the point measured throughout
this lesson) cannot pass without waiting; when `len(channel) == 0`, the channel is empty, and
a receive waits in the same way. These two functions are the inside-looking view of the
previous section's `howManyGoThrough` measurement: there, the number of attempts was counted
from outside; here, the channel's own state is read directly.

These functions have a limit: `len` only gives the value at the **instant** it is read, and
if more than one goroutine accesses the same channel at the same time, this value can have
already changed by the time it is read. On a channel used by a single goroutine, `len` is a
reliable observation; on a shared channel, it is only a snapshot, not a decision input.
Deciding to send, for instance, because `len(channel) < cap(channel)` holds true is not safe:
between this read and the send itself, some other goroutine can write to the same channel and
fill the buffer, and the send still waits — the "there is room" information `len` had just
given loses its validity somewhere between those two lines. `len` and `cap` are therefore an
observation tool, not a decision tool.

## Once a Slot Opens, an Attempt Passes Again

The buffer's fullness is not fixed — the slot that opens when a receiver takes a value out
of the buffer lets an attempt that was blocked before pass now. This, too, can be observed
inside a single goroutine, in sequence, requiring no concurrency at all.

```go
// slotopens.go — once a receiver takes a value from a buffered channel, the slot that opens lets an attempt pass again
package main

import "fmt"

func tryToSend(channel chan int, v int) bool {
	select {
	case channel <- v:
		return true
	default:
		return false
	}
}

func main() {
	channel := make(chan int, 2)
	channel <- 1
	channel <- 2

	thirdAttemptWhileFull := tryToSend(channel, 3)
	fmt.Println("did the third attempt pass while the buffer was full:", thirdAttemptWhileFull)

	receivedValue := <-channel
	fmt.Println("value received                                      :", receivedValue)

	thirdAttemptAfter := tryToSend(channel, 3)
	fmt.Println("did the same attempt pass once a slot opened        :", thirdAttemptAfter)
}
```

```
did the third attempt pass while the buffer was full: false
value received                                      : 1
did the same attempt pass once a slot opened        : true
```

**GC11.** — The channel with capacity `2` is filled with two values; the third attempt
falls to `default` because the buffer is full. Once a value is taken out with `<-channel`
(`1`, because a channel preserves entry order like a queue — this FIFO ordering is part of a
buffered channel's own definition, not a separate measurement), a slot opens in the buffer.
The same third attempt, with the source code entirely unchanged, now returns `true`. The
capacity stays fixed; what changes is the capacity's **fill level**.

The two measurements so far **tried** with `select`+`default`; when the attempt fails, no
send happens at all, and the program moves straight to the next line. But the real
consequence of the buffer filling up is that a send written directly, without `select`,
**genuinely waits** — exactly the behavior seen on the unbuffered channel in the previous
lesson. This cannot be shown inside a single goroutine, because the blocked party cannot
observe its own blocking; but it is possible to confirm, with a separate signal, that a
blocked send **completes for certain** once a slot opens.

```go
// blocking.go — once capacity fills a blocking send genuinely waits, and completes once the receiver takes a value
package main

import "fmt"

func main() {
	channel := make(chan int, 1)
	channel <- 1

	secondDone := make(chan struct{})
	go func() {
		channel <- 2
		close(secondDone)
	}()

	firstReceived := <-channel
	<-secondDone
	secondReceived := <-channel
	fmt.Println("first received :", firstReceived)
	fmt.Println("second received:", secondReceived)
}
```

```
first received : 1
second received: 2
```

**GC12.** — The channel with capacity `1` is filled with a single value (`1`). The started
goroutine's `channel <- 2` line cannot complete immediately, because the buffer is full, and
it waits there — the main goroutine does not observe this directly, since trying to (asking
"is it still blocked" with `select`+`default`, for instance) would itself be a race. Instead,
the main goroutine first takes the single value out of the buffer (`firstReceived`), opening
a slot for the second goroutine's waiting send, then waits on `<-secondDone` to be sure that
send has **certainly** completed. Because `close(secondDone)` runs **after** `channel <- 2`,
seeing this close guarantees the send has also gone through. The final receive
(`secondReceived`) is therefore `2` on every run.

## Bounding Measurement: The Buffer Does Not Remove Synchronization, It Defers It

The previous two measurements showed **when** the buffer blocks. This section measures what
the buffer **does not tell**: when a send to a buffered channel completes, the sending side
does not know that the value was actually read — it only knows that it found room in the
buffer. On an unbuffered channel, these two pieces of information would arrive on the same
line, because a send only completed once a receiver was ready. On a buffered channel, these
two moments come apart.

```go
// deferred.go — a send to a buffered channel completes but does not tell the sender that the receiver took the value
package main

import "fmt"

func main() {
	channel := make(chan int, 1)
	ack := make(chan struct{})

	channel <- 42
	fmt.Println("send completed (buffered)")

	go func() {
		<-channel
		close(ack)
	}()

	<-ack
	fmt.Println("now we know the receiver took the value (separate signal)")
}
```

```
send completed (buffered)
now we know the receiver took the value (separate signal)
```

**GC13.** — The `channel <- 42` line completes without waiting at all, since the capacity is
not full, and the "send completed" line is printed right after — at this point, no receiving
goroutine has even started yet. The **only way** to learn that the receiver has actually read
the value is a separate `ack` channel; without it in the code, the sending side would have no
way left to know when the receive happens, or even whether it happens at all. This shows, at
the same time, both the guarantee a buffered channel gives the sending side and its limit:
the guarantee is only "the buffer held this," and it carries no promise about the receive at
all.

This difference shows that the choice of which channel to use is an engineering decision. If
the sender needs to know that the receive has happened — to confirm that a job has started,
for instance — an unbuffered channel or a separate acknowledgment signal is needed. If it is
enough for the sender to just leave the value and move on, and the receiver does not need to
be ready at that moment, the buffer lets the sender proceed without waiting. Both
"synchronize" — but at different moments, and with different information.

This distinction is an example of the difference between "knowing it is ready" and "being
certain it will be ready." Without the `ack` channel, the sending side in `deferred.go` would
continue after leaving the value in the buffer without learning anything — not whether the
receiver runs, not when, not even whether it ever runs at all. As capacity grows, this
uncertainty does not go away, only the number of sends left carrying it grows: a hundred
sends in a row to a channel with capacity `100` each say "completed" separately, but give not
a single piece of information about how many were actually read.

This silence on a buffered channel's part is not a defect; what the channel commits to is
only "hold this value," never "tell you that the receiver took it." Code that wants that
second piece of information has to set it up separately, exactly as in this measurement.

This lesson's four measurements come together in a single line. `capacity.go` showed that
capacity is a limit, and that up to it a send passes without waiting at all. `lencap.go`
showed that this limit and the buffer's current fullness can be read from inside the
program. `slotopens.go` and `blocking.go` gave two views of crossing that limit: one an
attempt failing and moving on, the other a real send genuinely waiting until a slot opened.
`deferred.go`, in turn, showed that none of these four measurements tell the moment a send
is **received** — capacity, fullness, blocking are all measurable on the sender's own side;
when the receive actually happens stays outside all of them.

## Summary

- A channel's capacity determines where blocking starts: the first `capacity` sends pass
  without waiting at all, the ones after that wait for a receiver to arrive.
- A channel with zero capacity is not a separate type; an unbuffered channel is itself a
  channel with zero capacity.
- The slot that opens when a receiver takes a value out of the buffer lets an attempt that
  was blocked before pass now.
- A completed send to a buffered channel does not mean the receiver has read that value;
  this information can only be obtained with a separate signal.
- The choice between an unbuffered and a buffered channel is a design decision that depends
  on whether the sender needs to know the receive has happened.
- `len` gives the number of values currently waiting in a channel, `cap` gives the fixed
  capacity; together, they let the channel's instantaneous fullness be read without making
  an attempt.

## Next Step

Both channel types measured in this lesson were each waiting on only a single channel; the
difference between them was in when that channel made you wait. `select`+`default` was used
in this lesson only as an attempt tool on a **single** channel — to poll whether the capacity
was full. But a goroutine often waits on more than one source at the same time: one of
several result channels, a cancellation signal, a timeout. The next lesson measures this
broader form of `select` — when more than one channel is waited on together inside the same
`select`, whichever branch is ready gets chosen, and what happens when none of them are
ready.
