go 21 lines · 1 tab

Concurrency limiting with a context-aware semaphore

Leah Thompson Jan 2026
1 tab
package limit

import (
  "context"

  "golang.org/x/sync/semaphore"
)

type Limiter struct{ sem *semaphore.Weighted }

func New(max int64) *Limiter {
  return &Limiter{sem: semaphore.NewWeighted(max)}
}

func (l *Limiter) Do(ctx context.Context, fn func(context.Context) error) error {
  if err := l.sem.Acquire(ctx, 1); err != nil {
    return err
  }
  defer l.sem.Release(1)
  return fn(ctx)
}
1 file · go Explain with highlit

If you fan out work (HTTP calls, DB reads, image processing), the failure mode isn’t just “slow,” it’s “everything gets slow” because you saturate CPU or downstream connections. A semaphore is a simple way to cap concurrency. The important part is making it context-aware: Acquire should unblock when ctx.Done() fires, otherwise canceled requests still sit around waiting for tokens and you get work piling up after clients have gone away. I also release in a defer to make correctness boring. This pattern is useful inside handlers, background jobs, and queue consumers. It’s also measurable: you can export the current in-flight count and use it to understand saturation. Combined with timeouts, it gives you predictable load shedding instead of runaway goroutines.


Related snips

Share this code

Here's the card — post it anywhere.

Concurrency limiting with a context-aware semaphore — share card
Link copied