Day 076 — errgroup & Rate Limiting¶
Month 3 · Week 3 · ⬅ Day 075 · Day 077 ➡ · Journal index
🎯 Learning Objective¶
Run a group of goroutines, propagate the first error while cancelling the rest with errgroup.Group, and pace work with stdlib and token-bucket rate limiting.
📚 Topics¶
golang.org/x/sync/errgroup:Go,Wait,WithContext,SetLimit- First-error semantics + automatic context cancellation
- Rate limiting:
time.Tickerpacing (stdlib) vs token bucket (x/time/rate)
📖 Reading / Sources¶
📝 Notes¶
errgroup.Groupis "WaitGroup+ error handling".g.Go(func() error {...})launches a task;g.Wait()blocks for all and returns the first non-nil error (others are discarded) → [[errgroup]].errgroup.WithContext(ctx)returns a group and a derived context that is cancelled the moment any task errors (or whenWaitreturns). Tasks should select on that ctx so siblings stop early → [[context-cancellation]].g.SetLimit(n)bounds concurrency:g.Goblocks until a slot frees, turning the group into a built-in semaphore (Day 074) → [[bounded-concurrency]].g.TryGois the non-blocking variant.- errgroup lives in
golang.org/x/sync(an external module), so there's no stdlib-only runnable example — today's exercise re-implements the mechanism withsync+contextto prove I understand it → [[taskgroup]]. - Rate limiting caps how often work happens (events/sec), distinct from bounded parallelism which caps how many at once. The two compose.
- Stdlib pacing: a
time.Tickerfires everyd; receive fromticker.Cbefore each unit of work to enforce a steady rate. Alwaysdefer ticker.Stop()to free it → [[time-ticker]]. - A token bucket allows short bursts: tokens refill at rate
rup to capacityb; each event spends a token.golang.org/x/time/rate.Limiteris the canonical implementation (Wait,Allow,Reserve). ATickeris the no-burst (b == 1) special case. - Don't conflate the two limits: an
errgroupwithSetLimitbounds in-flight work; aLimiterbounds the start rate. A scraper often needs both → [[rate-limiting]].
💻 Code Examples¶
// errgroup: fan out, cancel-on-first-error, bounded to 4 concurrent tasks.
// import "golang.org/x/sync/errgroup" (external module — not stdlib)
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(4)
for _, u := range urls {
u := u
g.Go(func() error {
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("fetch %s: %w", u, err) // first error cancels ctx
}
return resp.Body.Close()
})
}
if err := g.Wait(); err != nil { // first error across all tasks
log.Println(err)
}
// Stdlib rate limiting (no burst): pace work to one unit per tick.
ticker := time.NewTicker(200 * time.Millisecond) // 5 events/sec
defer ticker.Stop()
for _, job := range jobs {
<-ticker.C // block until the next tick, enforcing the rate
process(job)
}
No runnable example today:
errgroupand the token-bucketrate.Limiterare ingolang.org/x/..., outside the stdlib. The exercisetaskgroupre-implements errgroup with onlysync+context.
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
Stdlib mini-errgroup (Go/Wait/SetLimit/WithContext) |
✅ | exercises/month-03/week-3/taskgroup/ |
🐛 Mistakes Made¶
- Returned the first error from
Waitbut forgot tasks keep running unless they watch the group's ctx — wired each task toctx.Done()so a failure actually cancels the rest. - Created a
time.Tickerand never calledStop()→ leaked the underlying timer in a long-running loop. Addeddefer ticker.Stop().
❓ Open Questions¶
- When is
rate.Limiter(burst) preferable to aTicker(steady)? (Bursty clients/APIs that allow a buffer of requests; revisit in the API month.)
🧠 Active Recall (answer without looking)¶
-
Q: What does
errgroup.Group.Waitreturn when three tasks fail, and what happens to the others after the first failure?
A
It returns the **first** non-nil error only (the rest are dropped). With `WithContext`, the first error cancels the derived context, so the other tasks — if they select on that ctx — stop early. -
Q: Rate limiting vs bounded parallelism — what does each cap?
A
Rate limiting caps *how often* events start (events/sec, e.g. a `Ticker` or token bucket). Bounded parallelism caps *how many* run at once (a semaphore / `SetLimit`). They're orthogonal and often used together.
🪶 Feynman Reflection¶
errgroup is a WaitGroup that also carries a clipboard: the first worker to fail writes the reason and flips a master switch (cancels the context) so everyone else can down tools. Rate limiting is a separate metronome — it doesn't care how many workers there are, only that a new beat (and thus a new task start) happens no faster than the set tempo.
🕳️ Knowledge Gaps¶
rate.Limiter'sReserve/Waitcancellation semantics under load — hands-on in the API month.
✅ Summary¶
I understand errgroup (first-error + cancel-the-rest + SetLimit bounding), proved it with a stdlib re-implementation, and can rate-limit with a time.Ticker (steady) or a token bucket (bursty), knowing rate vs concurrency are different limits.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 077: Week 3 review + closed-book recall over all the patterns.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: feat(exercises): stdlib mini-errgroup + rate-limiting notes (day 076)