# pond's second version added generics, which is where most of its weight is

> The Go worker pool is small, has no dependencies, and scales workers to zero. The interesting part is the version two list: typed task results, awaitable handles, panic recovery turned into errors, subpools with a fraction of a parent's workers, and bounded or unbounded queues. That is a library that added the things a first version always lacks.

**alitto/pond** — 🔘 Minimalistic and High-performance goroutine worker pool written in Go

- Repository: https://github.com/alitto/pond
- Stars: 2,197 · Forks: 87
- Language: Go
- License: MIT
- Published: 2026-09-30 · Updated: 2026-09-30 · Language: en
- Canonical page: https://hysenlabs.com/projects/alitto-pond

## Scaling to zero is the feature that distinguishes it from a semaphore

Installation is a single module fetch:

```bash
go get -u github.com/alitto/pond/v2
```

The pitch is a worker pool pattern: run many tasks while limiting how many goroutines execute at once, which is useful when you need to avoid resource exhaustion or rate limits. The readme names four use cases, and they are all the same shape: a large batch of work, a limit on concurrent outbound requests, a limit on concurrent database connections, and a rate-limited external interface. What makes this library more than a counting semaphore is in the third bullet of the feature list. Worker goroutines are created only when needed and removed immediately when idle, described as scaling to zero. A semaphore has no workers; it has permits. This has a pool that starts nothing, spawns a goroutine when work arrives, and reaps it when the work is done. The difference shows up in two places. Goroutine creation cost is paid per burst rather than held as idle overhead, and the number of live goroutines tracks the number of active tasks rather than your configured maximum. The second place is observability, because a pool can report how many workers are running and how many tasks are queued, which a semaphore cannot without extra bookkeeping around it. The readme also claims the library can outperform unbounded goroutines in some scenarios, which is the expected result of not creating ten thousand goroutines to do ten thousand simultaneous file reads.

## Six submission styles, and the one you will actually use

The readme's feature list describes six submission styles and the examples section shows six of them, which is a good ratio because the examples are the documentation. There is a plain submit that takes a function returning nothing, used when you do not care about the outcome. There is a submit variant for a function returning an error, which returns a handle you can wait on and get that error back. There is a result pool for a function returning a value, and the pool type is generic over the value type, so the compiler enforces the signature. There is a fourth combination for a function returning both a value and an error. There is submission associated with a context, where the context is passed into your task function and cancelling it stops the task and produces an error. And there is a group, which bundles related tasks and waits for all of them or for the first error. Read as a design, these are the three axes you actually vary: does the task produce anything, does it fail, and is it part of a set. A pool that made you choose one constructor per combination would be unusable, so the type parameter on the result pool is doing real work. The error-returning variants matter more than they look, because in Go a task that panics and a task that returns an error are different failure modes and a pool that only accepts the former forces you to write error plumbing in every closure.

## Panics become errors, which is a policy decision

One line in the version-two list deserves its own paragraph: panics are captured and returned as errors. That is convenient and it is also a choice with consequences. A panic in a goroutine that you spawned yourself crashes the process, which is a defensible default because a panic in a background task usually means a broken invariant and a crashed process surfaces it immediately. A pool that catches it converts that into a task that returned an error, which means the process keeps running and the failure is one line in your logs. For a long-running service processing thousands of tasks, that is almost always what you want: one malformed record should not take down the consumer. It also means a nil-pointer bug in a task closure becomes a per-item error rather than a crash, and if nobody is logging those errors you have a silent data-quality problem. The readme does not say what the error looks like or whether the stack is attached, and that is the first thing to check in the documentation, because an error without a stack trace is much harder to debug than a crash. Read this feature as a trade you are making deliberately rather than a convenience, and pair it with the context-based cancellation, which is the mechanism for stopping work that is no longer wanted.

## Subpools, queues, and resizing as the second-version headline

The remaining version-two features are about structure rather than about individual tasks, and they are what make the library usable in a real service. Subpools take a fraction of the parent pool's maximum workers, which solves a specific problem: you have a pool of a hundred workers and you want one component to be able to use at most ten of them without creating a second pool that can exceed the parent's limit. Dynamic resizing changes the maximum after construction, which matters when a deployment's limits are discovered at runtime or changed by configuration. Bounded versus unbounded queues is the choice with the sharpest consequences. A bounded queue applies back-pressure: when the queue is full, submission blocks or returns an error, and you choose which. An unbounded queue accepts everything immediately and lets memory grow, which is right when submission must not block and wrong when the downstream is slower than the producer. The readme's mention of blocking and non-blocking submission when the queue is full is the same decision at the submission call rather than at the pool level, so you can have a bounded queue and still choose per call whether a submit waits or fails. None of these existed in the first version, and together they are the difference between a demo and something you would put in front of traffic.

## Zero dependencies, a modest language floor, and a race-detector test target

The packaging is the easiest part of this project to evaluate and the most reassuring. The module file declares a module path with a major version suffix, a language floor, and nothing else. Zero dependencies means the entire library is the standard library, which for a concurrency library is a design statement as much as a packaging one: there is no third-party queue, no logging framework, no metrics library, and therefore no version conflict with anything else in your build. The readme's feature list leads with that fact, and the badges show coverage reporting and a static analysis report card, so there is evidence of testing rather than a claim of it. The build file has three targets and all three run the test suite with the race detector enabled, which for a library whose entire job is coordinating goroutines is the only test configuration that matters. Two details in those targets are worth naming. The local target uses a short timeout and a single run with caching disabled, which means every invocation is a genuine full run. The continuous integration target uses a longer timeout and repeats the suite three times, which is how you catch the intermittent failures that a concurrency library produces once and then hides. Coverage is a separate target writing an atomic-mode profile, which is the mode required when the test binary is parallel. An examples directory with six subdirectories, one per submission style, and an internal package complete the picture.

## Conclusion

Adopt pond if you are writing Go and need a bound on concurrency, because a semaphore channel is twenty lines and a pool is twenty lines plus the lifecycle you then wish you had: metrics, graceful stop, panic containment and a handle you can wait on. Do not adopt it if your concurrency is already handled by a semaphore and your tasks are fire-and-forget, since the pool earns its keep on shutdown behaviour and observability rather than on throughput. Four things to verify. Which major version your dependency is on, since the module path carries a version suffix and the second version is where the type-safe and awaitable features live. Whether the queue bounds suit you, because a bounded queue means submission blocks or errors when full and an unbounded one means memory grows with a slow downstream, and the readme offers both. What happens on a panic, since the readme says panics are captured and returned as errors, which is convenient and also means a bug that would have crashed your process now shows up as a failed task. And whether the pool is stopped, because a pool you never stop leaves goroutines alive and the readme's stop-and-wait call is what makes shutdown deterministic. The module declares a modest language floor, has no dependencies at all, and the test targets run with the race detector and no caching.

## FAQ

### What does pond do and when do I need it?

It manages concurrent tasks in Go with a bounded number of workers, creating worker goroutines only when needed and removing them when idle, so it scales to zero. The readme names four use cases: processing many tasks, limiting concurrent HTTP requests, limiting concurrent database connections, and calling a rate-limited interface.

### Does pond have any dependencies?

None. The module file declares a module path with a major version suffix, a language floor, and no requirements. The readme lists zero dependencies as the first feature, and the build targets run the test suite with the race detector enabled.

### What does pond do with a panic inside a task?

Panics are captured and returned as errors, so a failing task does not crash the process. The readme does not describe what the resulting error contains, so whether the stack trace is attached is the thing to check in the documentation before relying on it.

### What are subpools and bounded queues in pond?

A subpool takes a fraction of its parent pool's maximum number of workers, so a component can be capped without exceeding the parent limit. A bounded queue applies back-pressure when full, and the readme says submission can be blocking or non-blocking in that case, while an unbounded queue accepts work immediately and lets memory grow.

### How do I install pond?

Run the go get command for the versioned module path, which is the second major version where the type-safe and awaitable features live. The examples section shows six submission styles: plain, returning an error, returning a value, returning both, associated with a context, and as a group.

### What licence is pond released under?

MIT. Version 2.7.1 was released on 2026-04-14, preceded by 2.7.0 in March and 2.6.2 in February 2026, and the last push to the main branch was on 2026-06-22.

## Sources

- [alitto/pond on GitHub](https://github.com/alitto/pond)
- [Issues](https://github.com/alitto/pond/issues)
- [License: MIT](https://github.com/alitto/pond/blob/main/LICENSE)
- [README](https://github.com/alitto/pond/blob/main/README.md)
- [Releases](https://github.com/alitto/pond/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/alitto-pond
