Go's context Package and sync/atomic for Multi-Agent Token Budget Enforcement — Deadline Propagation, Atomic Counters, and the Per-Tenant Spend Guard
§I — Frame
The Ops lesson named three disciplines for keeping the context window from filling against the operator's will: count what goes in, compress before the limit, and evict by rank when compression falls short.
Go has two standard library packages that implement this pattern directly. The context package propagates the budget ceiling through the goroutine tree so that every downstream call site knows how many tokens remain. The sync/atomic package provides a lock-free counter that multiple goroutines can increment without acquiring a mutex. Together they produce a budget enforcer that adds no new dependencies and no shared mutable state beyond the counter itself.
Bodner's Chapter 10 on concurrency in Go identifies the two patterns that appear most often in production systems: cancellation propagation through context.Context, and atomic access to shared integers through sync/atomic (Learning Go, Ch. 10, pp. 229–248). The token budget enforcer is both patterns composed into one narrow shape.
§II — Language Idiom
context.Context. Go's context package solves the problem of carrying request-scoped values — deadlines, cancellation signals, named values — through a call chain without threading them through every function signature. A context.Context is created at the request boundary and passed as the first argument to every function that participates in the request. For token budgets, context.WithValue carries the per-request budget key. Sub-agents spawned by a fan-out goroutine receive a derived context carrying the same budget. They do not get a copy of the remaining budget; they get a pointer to the shared counter. The budget is a resource shared across the entire request tree, not divided among branches.
sync/atomic.Int64. Go 1.19 introduced typed atomic wrappers that replace the old function-based API with method syntax on a struct value. The struct is zero-initialized and ready to use. Calling Add(-tokenCount) decrements the counter atomically; calling Load() reads the current value without acquiring any lock. Two goroutines can decrement the same counter simultaneously without a race condition. The hardware guarantees the atomicity; Go's runtime does not add a lock on top.
The middleware wrapper. Rather than duplicating the budget-check logic at every LLM call site, a middleware wraps the function signature. The middleware reads the counter from the context, subtracts the estimated token cost, checks whether the result would go negative, and either proceeds or fires the gate callback. No individual call site knows about the budget; the middleware is the only place the policy lives.
§III — Code Worked Example
Three types compose the enforcer: a budget key type, a budget struct carrying the atomic counter, and the middleware wrapping the LLM call.
package budget
import (
"context"
"errors"
"sync/atomic"
)
type budgetKey struct{}
type Budget struct {
remaining atomic.Int64
gateFunc func(ctx context.Context) error
}
func New(tokens int64, gate func(ctx context.Context) error) *Budget {
b := &Budget{gateFunc: gate}
b.remaining.Store(tokens)
return b
}
func (b *Budget) WithContext(ctx context.Context) context.Context {
return context.WithValue(ctx, budgetKey{}, b)
}
func FromContext(ctx context.Context) (*Budget, bool) {
b, ok := ctx.Value(budgetKey{}).(*Budget)
return b, ok
}
var ErrBudgetExhausted = errors.New("token budget exhausted")
func (b *Budget) Consume(ctx context.Context, estimatedTokens int64) error {
after := b.remaining.Add(-estimatedTokens)
if after < 0 {
b.remaining.Add(estimatedTokens)
if b.gateFunc != nil {
return b.gateFunc(ctx)
}
return ErrBudgetExhausted
}
return nil
}
func (b *Budget) Remaining() int64 {
return b.remaining.Load()
}
The Consume call is the accounting step from the Ops lesson. It subtracts optimistically, then rolls back if the result would go negative. The rollback is atomic: Add(estimatedTokens) is the exact inverse of Add(-estimatedTokens), and no other goroutine can observe the intermediate negative value before the rollback fires. The gateFunc is the hook where the summarization gate callback lives — the caller decides what happens when the budget runs out.
The middleware wrapping the LLM call site:
func WithBudgetCheck(
estimatedTokens int64,
call func(ctx context.Context) (string, error),
) func(ctx context.Context) (string, error) {
return func(ctx context.Context) (string, error) {
b, ok := FromContext(ctx)
if !ok {
return call(ctx)
}
if err := b.Consume(ctx, estimatedTokens); err != nil {
return "", err
}
return call(ctx)
}
}
The middleware is composable: wrap any LLM-calling function with WithBudgetCheck and the accounting fires transparently. The inner function does not know a budget exists. If no budget is in the context, the middleware passes through unchanged.
Fan-out with the shared counter:
func RunFanOut(ctx context.Context, b *Budget, tasks []Task) []Result {
budgetCtx := b.WithContext(ctx)
g, gCtx := errgroup.WithContext(budgetCtx)
results := make([]Result, len(tasks))
for i, task := range tasks {
i, task := i, task
g.Go(func() error {
out, err := WithBudgetCheck(task.EstimatedTokens, task.Execute)(gCtx)
if err != nil {
return err
}
results[i] = Result{Output: out}
return nil
})
}
g.Wait()
return results
}
All goroutines share the same Budget pointer. When one goroutine's Consume call brings the counter below zero, subsequent calls by other goroutines also hit the rollback path and fire the gate. The budget is a fleet-wide resource for this request, not a per-goroutine allowance.
§IV — Connection to Today's Ops Lesson
The Ops lesson's token accounting step is budget.Consume, called before context assembly. The summarization gate is gateFunc: the caller passes a function that runs the summarization call, compresses old turns, and returns the new headroom count. Priority-truncation lives in the gate function's implementation — if the summarized context is still too large, the gate function calls the eviction policy before returning.
The middleware wrapper is the single choke point the Ops lesson required: "the gate fires without changing the logic at any individual call site." Individual agents call task.Execute(ctx). The middleware, not the agent, carries the policy.
§V — Prior-Lesson Reach
The 06-19 FSM lesson used atomic.Pointer for lock-free state reads. This lesson uses atomic.Int64 for lock-free counter updates. Both are the same package, the same hardware guarantee, the same no-mutex contract. The FSM's Load() is a non-blocking read of the current state; Budget.Remaining() is the same primitive on a different type.
The 06-09 reverse-proxy lesson showed that middleware wrapping is how Go production systems attach cross-cutting policy to HTTP handlers without modifying the handlers. WithBudgetCheck is the same pattern applied to a function type rather than an http.Handler. The wrapping composes: chain multiple WithBudgetCheck calls with different estimated costs for different call types.
§VI — Closing
Two standard library packages. One atomic counter. One context value. The budget enforcer is three hundred lines of Go at most and adds nothing to the dependency graph.
The context package propagates the ceiling. The atomic counter enforces it. The middleware is where the policy lives. The gate function is where the summarization fires. The pieces compose because Go gives each piece exactly one job.
Examine well.
Related
- Prior arc: Go FSM and atomic.Pointer (2026-06-19) — prior Go lesson; atomic.Pointer for state; atomic.Int64 for counter today
- Language hub: Go (Polyglot-Dev)
- Grounding tome: Learning Go — Jon Bodner (Ch. 10 Concurrency in Go, pp. 229–248)