Library / SDK
panjf2000/ants avatar
panjf2000/ants

ants caps concurrent goroutines at a number you pick, then recycles the idle ones

🐜🐜🐜 ants is the most powerful and reliable pooling solution for Go.

14,521 stars1,439 forksGoMIT

At a glance

What is it?
ants is a fixed-capacity Go goroutine pool from panjf2000 that bounds concurrency, purges workers that go idle, and offers tuning, release and reboot at runtime. What it does not give you is task ordering or task results.
Who is it for?
ants fits a Go service whose fan-out is unbounded but whose concurrency budget is not, and whose owners will write their own completion channel because the pool does not provide one. Leave it out when task order matters, when you need a result or an error back per task, or when a swallowed panic would be worse than a crash.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 56 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • panjf2000/ants14,521 ★ · +11
Star history
Repository2026-09-222026-11-14
panjf2000/ants14.5k14.5k

11 Stars in the last 53 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

What ants bounds is concurrency, not throughput

The pool exists to replace one-goroutine-per-task with a fixed capacity. NewPool takes the number you want:

go
p, _ := ants.NewPool(10000)

Tasks then go to ants.Submit rather than to the go keyword. What you get back is a bound on how many tasks run at once, and what you give up is the scheduler's freedom to expand. If your workload has a natural ceiling, such as a fixed number of upstream connections, that is a real saving in memory and in contention.

The alternative most teams already have is plain goroutines, which cost memory in proportion to concurrency rather than a constant. A semaphore over golang.org/x/sync gives the same bound without a pool, and other worker-pool libraries implement the same idea with their own APIs. What distinguishes ants is the surface around the bound: tuning, purge, release and reboot in one package.

Its runtime surface is small. go.mod declares Go 1.19 and requires stretchr/testify v1.10.0 and golang.org/x/sync v0.11.0, with nothing else at the top level.

Purging overdue goroutines is what separates a pool from a slow leak

Workers in a fixed-capacity pool have two states: running a task, or waiting for one. The waiting state is where the memory goes, and a pool that never reclaimed them would hold its full capacity forever whether or not traffic arrived.

That is the purge step. Overdue goroutines are collected periodically, so a burst that once needed 10000 workers releases them again instead of parking them for the life of the process. This is the mechanism that makes a large declared capacity cheap when average load is low.

The consequence for a reader is about the capacity number itself. Setting 10000 because you once had a spike means you are choosing a ceiling, not an allocation, and the difference between those two readings decides whether the pool costs you anything during quiet hours.

Submit hands back no ordering and no result

The usage example is one line, and it shows both limits:

go
ants.Submit(func(){})

Nothing captures a value and nothing waits. A separate note explains why: tasks submitted to the pool are not guaranteed to be addressed in order, because they scatter among a series of concurrent workers and execute concurrently. ants is not a work queue with FIFO semantics, and code that assumes submission order equals execution order will be wrong under load.

The missing result is the other half of the same gap. To learn that a task finished, or what it produced, you have to build the channel or WaitGroup yourself inside the closure. That is not a criticism of the design so much as a cost to budget: every call site owns its own completion plumbing, and a fire-and-forget submit has no defined moment where you learn it failed.

If you need ordering or results, you are assembling a queue library on top of a worker library.

A panicking task comes back looking like a task that succeeded

Graceful panic handling is on the feature list, phrased as preventing programs from crashing. For a long-running service that is the behaviour you want: one bad request should not take the process with it.

It also removes the signal you would normally rely on. When a panic is recovered inside the pool, the caller of Submit gets nothing back, so a task that died mid-flight and a task that ran to completion look identical from the outside.

Before relying on the recovery feature, check ants.Options for how a recovered panic is reported and verify it in your own code: find the log line, or confirm that your closure wraps its own recover and records the failure, because otherwise a panic becomes a silent drop.

Release is not terminal, because Reboot brings the pool back

The lifecycle has three verbs and one of them undoes the first. Immediate shutdown is pool.Release, and the bounded version is:

go
pool.ReleaseTimeout(time.Second * 3)

Resize while running is a different call:

go
pool.Tune(1000) // Tune its capacity to 1000
pool.Tune(100000) // Tune its capacity to 100000

The README states the tuning method is thread-safe, which is what makes it safe to call while traffic is in flight. What it does not state is what ReleaseTimeout does to tasks still running when the timeout expires: whether they are cancelled, orphaned, or waited on. If your shutdown path relies on draining work, that behaviour is the piece to confirm in the source.

Reboot is the sharpest edge. A released pool stays usable:

go
// A pool that has been released can be still used after calling the Reboot().
pool.Reboot()

A Release call in one place is therefore not proof the pool is gone, and a component that reboots on demand can hand you a live pool you thought you had shut down.

WithPreAlloc buys the whole ring buffer at NewPool time

Preallocation is opt-in, and the constructor shows the price:

go
// ants will pre-malloc the whole capacity of pool when calling ants.NewPool.
p, _ := ants.NewPool(100000, ants.WithPreAlloc(true))

The whole capacity, allocated when the pool is constructed rather than as work arrives. The case the README describes for it is narrow: a pool with ultra-large capacity where each task lasts a long time, where pre-mallocing removes a lot of allocation from the goroutine queue.

Read the trade-off honestly. On a long-running service with bursty traffic, turning this on means holding memory for 100000 workers from the first request, which is the opposite of what the purge mechanism is for. It pays off when the queue would otherwise churn allocations under sustained load, and costs you when the peak is theoretical.

The feature list also claims a pool may achieve higher performance than unlimited goroutines in Go. That is the README's own hedged wording, so treat it as a direction rather than a number.

v1 and v2 are separate modules with separate capacity limits

Two import paths exist and the README gives an install command for each:

powershell
go get -u github.com/panjf2000/ants

That is v1. For v2:

powershell
go get -u github.com/panjf2000/ants/v2

go.mod in the v2 tree declares the module as github.com/panjf2000/ants/v2 and requires Go 1.19, so 1.19 is the floor for the current line.

The consequence of the split is the one to plan around. A project that has not migrated carries both modules in its build, and each pool keeps its own workers and its own capacity. A concurrency limit set on the v1 pool does nothing to the v2 pool, and a bug report about unbounded goroutines becomes a question of which import a given package used. Grep the imports before assuming a single global bound.

The README also carries a Minimum Go Version badge that points at panjf2000/gnet, a different repository, so the authoritative floor for this module is the go directive in go.mod rather than that link.

Three queue implementations sit in the tree, and the README names none as default

The repository is a flat package at the root. Alongside ants.go, pool.go, options.go and worker.go you get pool_func.go, pool_func_generic.go and matching worker variants, then three queue files: worker_queue.go, worker_stack.go and worker_loop_queue.go, each with its own test file. There are also multipool.go, multipool_func.go and multipool_func_generic.go. The constructors named in the docs are NewPool, NewPoolWithFunc and NewPoolWithFuncGeneric, and ants.Options carries every optional setting applied through option functions in those three.

That much surface says the library has opinions about structure, and it says nothing about which queue NewPool picks. Nothing in the README maps a configuration to a queue implementation, so if the default does not fit a workload, the selection point is the one thing you will have to find in the source.

On cadence, three tagged releases are visible: v2.10.0 on 2024-06-18, v2.11.0 on 2025-01-12 and v2.12.0 on 2026-03-20. The default branch is dev rather than master, and the last push landed on 2026-09-19. The repository is not archived, but go get resolves to the newest tag, not to the branch head.

Editorial conclusion

ants fits a Go service whose fan-out is unbounded but whose concurrency budget is not, and whose owners will write their own completion channel because the pool does not provide one. Leave it out when task order matters, when you need a result or an error back per task, or when a swallowed panic would be worse than a crash. Verify two things first: which queue implementation NewPool selects, since three exist as separate files and the README does not say which is default, and whether you actually need v2, since importing v1 and v2 in one binary gives you two pools with two independent capacity limits. The tagged release v2.12.0 is dated 2026-03-20 and the last push to the default dev branch was 2026-09-19, so decide whether you pin a tag or track the branch.

Frequently asked questions

How do I install ants v2 in a Go project?

Run go get -u github.com/panjf2000/ants/v2. The v1 line is a different module at github.com/panjf2000/ants, and go.mod for v2 declares Go 1.19 as its floor.

Does ants run submitted tasks in order?

No. The README states that tasks submitted to the pool are not guaranteed to be addressed in order, because they scatter among a series of concurrent workers and run concurrently.

Can I change the ants pool size while it is running?

Yes. pool.Tune resizes a live pool and the README calls the method thread-safe. Its own examples move a pool from 1000 to 100000 workers.

What happens when a task panics inside ants?

Graceful panic handling is listed as a feature, to prevent programs from crashing. Nothing in the usage example captures a result; ants.Options holds the pool's optional configuration.

Can I reuse an ants pool after releasing it?

Yes. pool.Release shuts a pool down and pool.ReleaseTimeout(time.Second * 3) waits up to a limit, and a released pool can be used again after pool.Reboot().

Official sources

  1. License: MIT
  2. panjf2000/ants on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/panjf2000-ants.svg)](https://hysenlabs.com/projects/panjf2000-ants)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/panjf2000-ants.svg)](https://hysenlabs.com/projects/panjf2000-ants)

Was this useful?