I got tired of rewriting the same concurrency limiter

Why I packaged asyncq instead of pasting another half-finished helper into the next side project.

I did not wake up wanting to compete with p-limit.

I woke up tired of meeting the same problem in a slightly different costume and solving it like I had never seen it before.

The costume changes. Sometimes it is a batch of fetches against an API that punishes greed. Sometimes it is a local script that should process a folder without opening every file handle at once. Lately it is agent-style loops that try to run every tool call in the same breath and then act surprised when cancel is messy or the provider starts saying no.

The bad habit stays the same. I reach for Promise.all, it works on the demo input, it falls over on the real list, and I go hunting through old repos for “that concurrency helper.” Half the time I find something I wrote on a weeknight, barely commented, missing the one behavior I need this time. Clear the rest of the queue. Wait until idle. Peek at how many are actually running. Turn the dial down after a 429 without rewriting every call site.

That is the real origin of asyncq. Not a market gap presentation. Not a hot take that p-limit is wrong. Just me admitting I was going to keep needing a small, boring limiter under my own roof.

Why bother packaging something this small

Because “small” is exactly when I get lazy.

A twelve line helper feels too minor to deserve a repo, so it lives in utils, or a gist, or a private project I cannot link later. Then six months pass, I need it again, and I rewrite it worse. Or I copy it and accidentally drag along whatever assumptions the last project had.

Packaging forces a different bar. Tests. A public API I have to live with. A README that has to make sense to someone who is not me at 11pm. Zero dependencies, on purpose, because the whole point of a tiny tool is that it should be safe to drop into a script without negotiating a dependency tree.

I also care about the lane I am trying to build in public. I like shipping small TypeScript utilities that are honest about what they are. asyncq is the first clean mark on that board under justinwilliams-io. If the only outcome is that future-me stops spelunking old folders, the package already paid rent.

Why not just use p-limit

Most of the time, you should.

If p-limit is already in your lockfile and it covers your life, stay there. I mean that without the fake humility dance. Switching packages to feel clever is a waste of a PR.

I still wanted my own package for a few selfish reasons. I wanted the behaviors I keep reaching for in one place without extra packages for inspection or idle wait. I wanted zero runtime deps for throwaway scripts and demos. I wanted something I could evolve with my own taste, including the agent-adjacent patterns I keep bumping into, without turning the core into a framework.

There is also a craft reason that is harder to defend in a feature matrix. I learn more from finishing a tiny public tool end to end than from endlessly wiring other people’s primitives into side projects and never putting my name on the boring parts.

Why agents made this feel timely

I have been around more tool-calling loops lately. They make unbounded concurrency look productive for about five minutes.

Then the run becomes hard to reason about. Too many calls in flight. Stop means “please stop” instead of “the queue actually stopped.” One global free-for-all mixes cheap file reads with expensive model calls and serial browser steps like they cost the same.

I do not think the answer is another agent framework for this. The answer is the same old discipline with a clearer motivation: decide how much parallelism you can afford, separate the pools when the bottlenecks differ, and make cancel a first-class idea instead of an afterthought.

That is why asyncq has room for clear, idle, abort on pending work, and a concurrency knob you can move when a provider is angry. Not because those features look impressive in a table. Because those are the moments where my old private helpers always felt unfinished.

There is an optional Agent Skill in the repo for the same reason. Coding agents love reinventing queues mid-task. I would rather point them at a package that already exists. The skill is just instructions. Someone still installs the dependency.

What I am deliberately not optimizing for

I am not trying to win a popularity contest against the sindresorhus ecosystem.

I am not building a rate limiter. Concurrency is how many at once. Quotas and token buckets are a different problem, and pretending one package should own both usually makes the API worse.

I am not building a job platform. Retries, persistence, dashboards, and workers are real needs. They are just not this need.

If those boundaries make asyncq feel small, good. Small was the point.

Where the how lives

If you want install lines, API details, and copy-paste examples, use the README and the npm page. That is the reference. This post is the why.

npm install @justinwilliams-io/asyncq

I will keep writing here the way I wish I had journaled the first time through a problem: public work, full sentences, and fewer recycled README sections. FormBeam is still my solo product (formbeam.io), and the tiny tools lane is how I want the engineering side of my name to show up outside of day jobs and pitch pages.