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 line to bound the queue, one line to specify what happens when it's full. The strategy is part of the actor definition, visible to anyone reading the code.

backpressure.hew
// Bounded mailbox: drops new messages when full
actor SlowWorker {
    mailbox 4;
    receive fn process(value: Int) {
        sleep_ms(50);
        println(f"processed: {value}");
    }
}
// Coalesce: keep only the latest per key field
actor PriceTracker {
    mailbox 2 overflow coalesce(symbol);
    let symbol: String;
    let price: Int;
    receive fn update(symbol: String, price: Int) -> Int {
        this.symbol = symbol;
        this.price = price;
        price
    }
}

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), you need a custom goroutine with a map and a ticker, plus shutdown logic.

backpressure.go
// 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's mailbox declarations are declarative and local to the actor. You can read the backpressure strategy without looking at any other file. Go's backpressure logic is spread across goroutines, channels, and coordination code — often in a different function from the worker it protects.

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. Hew's default overflow drops new messages, though other strategies are available (drop_old, coalesce, block, fail). Blocking is safer by default; dropping is faster but requires the developer to handle message loss.