In a channel-based concurrency model, what is the difference between an unbuffered (rendezvous) channel and a buffered one, and what does each guarantee about when the sender resumes?
answer
- capacity 0 = rendezvous = both sides meet
- buffered = accepted, not delivered
- capacity = how far producer may run ahead
- full buffer still blocks = flow control kept
- unbounded = memory growth, no pushback
basics
~20 sAn unbuffered channel is a rendezvous: the send completes only when a receiver takes the value, so both sides synchronize. A buffered channel lets the sender resume as soon as the value fits in the buffer, and blocks only when it is full.
solid answer
~60 sAn unbuffered channel has capacity zero. A send does not complete until some receiver is taking the value, so the two tasks meet at that point — a rendezvous. That gives a real synchronization fact: when the send returns, the handoff happened, so it doubles as an acknowledgement and imposes lockstep pacing. A buffered channel of capacity N is a bounded queue. The send returns as soon as the value is stored, so the producer may run up to N items ahead of the consumer; it blocks only when the buffer is full. That absorbs burstiness and reduces context switches, but the sender learns nothing about delivery: the value can sit in the buffer, and if the process dies it is lost. Capacity is therefore a design parameter meaning how far may the producer run ahead. Zero means strict pacing and maximum coupling. Small N smooths jitter while keeping flow control, because a full channel still blocks the producer. Unbounded means no flow control at all: a slow consumer becomes unbounded memory growth instead of visible pushback.
code
text · 11 linesch = channel(capacity = 0)
send(ch, 1) # blocks forever if nobody is receiving
ch = channel(capacity = 1)
send(ch, 1) # returns immediately, value sits in buffer
send(ch, 2) # blocks: buffer full
# acknowledgement must be modelled explicitly on a buffered channel
reply = channel(0)
send(work, Job(payload, reply))
receive(reply) # now you know it was processed, not just acceptedgo deeper
State the mechanical rule: capacity 0 means send waits for a receiver; capacity N means send waits only when N items are already queued.
Add what the sender learns in each case (handoff happened vs merely accepted) and that a full buffer still blocks, which is where flow control comes from.
Talk about capacity as a tuning knob for burst absorption, the latency cost of depth, and why unbounded queues turn a rate mismatch into an outage.
Frame capacity as where you want a rate mismatch to become visible — immediately at the producer, or later as latency and memory — and tie it to end-to-end latency objectives and shedding policy.
## Rendezvous semantics An unbuffered channel has no storage. A send and a receive must both be present for either to complete; whichever arrives first waits. The value moves directly from sender to receiver in one event. The important consequence is informational: after the send returns, the sender knows a receiver has the value. That is an acknowledgement built into the primitive, and it is why rendezvous channels are useful as pure signals, for example a done channel where the value carries no data and only the meeting matters. A timeline makes the coupling visible. unbuffered: S: send(x) ....blocked.... | handoff | continue R: recv() ......| handoff | continue buffered(2): S: send(x) continue, send(y) continue, send(z) BLOCKED R: (busy) ................................ recv() -> x ## Buffered semantics A channel of capacity N holds up to N values. Send stores and returns while there is room; receive takes the oldest value, normally in FIFO order. The sender and receiver are decoupled in time by up to N items. The sender now knows only that the value was accepted for delivery, not that anyone consumed it. If you need the acknowledgement you must model it: send a reply channel with the message and wait on it. Confusing accepted with processed is the single most common error — it appears as work reported as done that never ran because the process exited with items still in the buffer. ## Why capacity is a design decision Capacity answers how far may the producer run ahead of the consumer. - Zero: strict pacing. Every item is a synchronization point. Highest coupling, lowest memory, most context switches, and stalls travel upstream immediately. - Small N (typically the size of a burst, or a small multiple of the number of workers): absorbs jitter so a momentarily slow consumer does not stall the producer, while keeping flow control because a full buffer still blocks. Fewer wakeups per item, so throughput usually improves. - Large N: hides a throughput mismatch for a while and then fails anyway, with the buffer full and latency inflated by the queue. - Unbounded: not a buffer but a memory leak with extra steps. If the average consumption rate is below the production rate, occupancy grows without limit; the system dies from memory exhaustion instead of pushing back. The queueing view is useful. Latency through the stage is roughly queue occupancy divided by service rate, so by Little's law a deeper buffer buys throughput smoothing and pays in latency. Buffering never fixes a stage that is permanently slower than its producer; it only converts a rate mismatch that would have shown up as immediate blocking into a delayed, larger failure. ## Correctness effects Buffering changes which programs deadlock. Two processes that each send before receiving deadlock on unbuffered channels but succeed if each channel has room for the outstanding sends. That is real, but it is a fragile fix: it works only while the number of outstanding messages stays under the capacity, so it hides the cyclic topology rather than removing it. Buffering also weakens the ordering you can assume between two different channels. With a rendezvous, the receiver's progress is pinned to the sender's; with a buffer, the sender may be far ahead, so events observed on two channels may interleave in ways that a lockstep design never showed. ## Choosing Use unbuffered when the handoff is the point: signalling completion, request/reply, or enforcing strict pacing so that a downstream stall is felt upstream immediately. Use a small bounded buffer for throughput pipelines where producer and consumer are both bursty. Never use unbounded unless the input itself is provably bounded and small.
- Someone reports that adding a buffer of 1024 fixed their intermittent hang. Should you keep the change?Treat it as a diagnostic, not a fix. Buffering only postpones a cyclic or unmatched communication: it works while fewer than 1024 messages are outstanding and hangs again under load or a slower consumer. Find the topology problem, then choose a capacity for throughput reasons rather than to avoid a hang.
- How would you pick a capacity for a real pipeline stage?Size it to the burst you want to absorb, not to the total volume: roughly the number of items that arrive during one consumer stall, often a small multiple of the worker count. Then measure occupancy in production; a buffer that is usually empty is correctly sized, one that is usually full means the consumer is the bottleneck and no capacity will help.
Rendezvous is handing a document to a colleague face to face: you do not walk away until they have it. A buffered channel is their inbox tray with room for two: you drop it and go, and if the tray is full you must wait at their desk.
saying these in an interview costs you the question
- Believing a completed send on a buffered channel means the receiver processed the message.
- Using an unbounded queue and calling it backpressure-friendly — it removes flow control entirely.
- Thinking a bigger buffer improves latency; it improves smoothing and usually increases latency.
- Claiming an unbuffered channel is just a buffered channel with capacity 1 — capacity 1 lets the sender run one item ahead and provides no rendezvous.