Backpressure
Fast producer, slow consumer. Without backpressure, unbounded queues grow until the process runs out of memory. Every concurrent system needs a strategy for this.
Hew
Mailbox size and overflow strategy are declared on the actor. One-way calls through mailbox(actor) use that admission policy; ordinary actor calls wait for handler completion.
// Bounded mailbox: drops new messages when full
actor SlowWorker {
mailbox 4 overflow drop_new;
receive fn process(value: i64) {
sleep(50ms);
println(f"processed: {value}");
}
}
// Coalesce: keep only the latest update per symbol key
type PriceUpdate { symbol: string; price: i64; }
actor PriceTracker {
mailbox 2 overflow coalesce(symbol) fallback drop_new;
var last_price: i64 = 0;
receive fn update(symbol: string, price: i64) {
_ = symbol;
last_price = price;
}
}
fn main() {
let worker = spawn SlowWorker;
let inbox = mailbox(worker);
for value in 0..10 {
// Submit without waiting for process() to finish. When the mailbox is
// full, its drop_new policy may return Delivery.Discarded.
_ = inbox.process(value);
}
}Go
Go's buffered channels provide basic bounding — the sender blocks when the channel is full. For coalescing (keeping only the latest value per key), a typical Go implementation uses a custom goroutine with a map and a ticker, plus shutdown logic.
// Bounded: buffered channel blocks the sender
ch := make(chan int, 4)
// Coalesce: custom goroutine with dedup logic
type PriceUpdate struct {
Symbol string
Price int
}
func coalescer(in <-chan PriceUpdate, out chan<- PriceUpdate) {
latest := make(map[string]PriceUpdate)
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
for {
select {
case update, ok := <-in:
if !ok {
return
}
latest[update.Symbol] = update
case <-ticker.C:
for _, u := range latest {
out <- u
}
latest = make(map[string]PriceUpdate)
}
}
}
func main() {
in := make(chan PriceUpdate, 100)
out := make(chan PriceUpdate, 100)
go coalescer(in, out)
// ...
}What this shows
Developer experience
Hew keeps the destination's default capacity and admission policy in the actor declaration; the sender selects one-way submission with mailbox(actor). Go expresses the same decisions through channel capacity plus the surrounding send and receive control flow.
Debugging
In Hew, if messages are being dropped, the mailbox size and overflow strategy are visible in the actor definition. In Go, backpressure bugs are scattered across goroutine interactions — you have to trace channel reads, writes, and buffer sizes across multiple functions.
Trade-offs
Go's buffered channels block the sender, which provides natural flow control — producers slow down to match consumer speed. The Hew examples make the loss/blocking choice explicit (drop_old, coalesce, block, fail). Blocking protects delivery at the cost of throughput and tail latency; dropping or coalescing can be useful for lossy streams, but only when message loss is part of the contract.