npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@icazemier/sway

v2.1.0

Published

Adaptive concurrent task runner — like Promise.all() with gradient-based concurrency control

Downloads

340

Readme

Sway

Promise.all() with adaptive concurrency control. Zero dependencies.

Why

Concurrency is often an improvement in async code (e.g. Promise.all() ). But often one has too many concurrent tasks, ok... Too many, how many is too many? So to solve this, one implements a way to "limit" concurrency. Then again, what would the maxConcurrency be, 4 or 8?

Sway just puts 2 ideas together, (1st) an adaptive controller to control the (2nd) maxConcurrency and uses a feedback loop to adapt.

Sway automatically finds the optimal number of concurrent tasks -- you don't have to guess. Under the hood it uses a latency-gradient algorithm inspired by TCP Vegas and Netflix's adaptive concurrency limiter.

Quick start

npm install @icazemier/sway
import { sway } from '@icazemier/sway';

const { results, stats } = await sway(
  urls.map(url => () => fetch(url).then(r => r.json()))
);

console.log(results);               // resolved values in original order
console.log(stats.peakConcurrency);  // highest concurrency reached
console.log(stats.avgConcurrency);   // average concurrency across the run

For the common case of mapping items to async work, use sway.map:

const { results } = await sway.map(
  urls,
  async (url) => (await fetch(url)).json()
);

That's it. No concurrency number to pick -- sway figures it out.

Is sway right for me?

Think of concurrency like lanes on a highway:

  • Too few lanes (low concurrency) -- cars crawl, road is underused
  • Too many cars (high concurrency) -- traffic jam, everything slows down
  • Sweet spot -- maximum flow

With a fixed pool you're guessing how many lanes to open. Guess wrong and you either waste capacity or cause a jam. Sway figures out the right number of lanes while driving.

The one-liner test: "If I doubled the concurrency, could things get slower?"

If yes -- sway is for you. If no -- just use Promise.all().

Use sway when

  • You're hitting an API and don't know its rate limits
  • You're querying a database and don't know the connection pool sweet spot
  • You're processing files on disk and don't know how many parallel reads the drive handles well
  • Your tasks hit different services with varying capacity
  • You're writing a library or tool where the end user's infrastructure is unknown

Don't use sway when

  • You already know the exact right concurrency (just use a fixed pool)
  • Your tasks are pure computation with no I/O (concurrency = CPU cores, done)
  • You have very few tasks (< 20) -- not enough for the controller to learn

Usage

Basic

Pass an array of task functions (thunks). Each thunk is a zero-argument function that returns a promise:

const tasks = urls.map(url => () => fetch(url));
const { results } = await sway(tasks);

With options

const { results, stats } = await sway(tasks, {
  maxConcurrency: 16,    // never run more than 16 at once
  initialConcurrency: 2, // start cautiously
});

Error handling

Sway rejects on the first error, just like Promise.all():

try {
  await sway(tasks);
} catch (err) {
  // first task rejection
}

Generators and iterables

Accepts any Iterable -- arrays, generators, or custom iterables. Tasks are pulled lazily from the iterator, so you can feed millions of tasks without building the full array in memory:

function* generateTasks() {
  for (const id of ids) {
    yield () => processItem(id);
  }
}

const { results } = await sway(generateTasks());

Stats

Every run returns a stats object with performance telemetry:

const { stats } = await sway(tasks);

stats.totalTasks;      // number of tasks executed
stats.totalDurationMs; // wall-clock time in ms
stats.peakConcurrency; // highest concurrency reached
stats.avgConcurrency;  // weighted average concurrency
stats.adjustments;     // how many times the controller changed level

sway.allSettled — never reject on task failure

Mirrors Promise.allSettled: run every task to completion and get a settle-result per input, even when some tasks throw.

const { results } = await sway.allSettled(
  urls.map(url => () => fetch(url))
);
for (const r of results) {
  if (r.status === 'fulfilled') console.log(r.value.status);
  else console.error(r.reason);
}

The returned promise never rejects on task failure -- only if the input iterator itself throws.

sway.map — ergonomic mapping shortcut

Pass items and an async function directly, like Array.prototype.map. Saves the thunk-wrapping boilerplate.

const { results } = await sway.map(
  items,
  async (item, index) => process(item)
);

Items are pulled lazily from the source iterable, so generators with millions of items still work.

Options

All values are counts or ratios -- no time-based units to worry about.

| Option | Default | Description | | -------------------- | ------- | ---------------------------------------------------- | | maxConcurrency | 64 | Upper bound for concurrent in-flight tasks | | minConcurrency | 1 | Lower bound for concurrent in-flight tasks | | initialConcurrency | 4 | How many tasks to start with | | smoothingFactor | 0.3 | Latency EMA responsiveness (0-1), lower = calmer | | probeInterval | 8 | Completed tasks between concurrency adjustments |

Tip: The defaults work well for most workloads. Start without options and only tune if you see a reason to.

How it works

Most concurrency controllers use throughput (tasks/sec) to decide when to scale up or down. The problem: throughput is a lagging indicator -- it only drops after you've already overshot, and by then back-pressure has already piled up.

Sway uses latency instead. Latency is a leading indicator -- it rises immediately when concurrency exceeds the sweet spot, because tasks start queueing before throughput visibly drops. This is the same insight behind TCP Vegas and Netflix's adaptive concurrency limiter.

The controller tracks two values:

  • minLatency -- the lowest task duration observed (learned no-contention baseline)
  • latencyEma -- an exponential moving average of recent task durations

Every probeInterval completions it computes:

gradient    = minLatency / latencyEma          // 0..1, where 1 = no contention
newLimit    = concurrency × gradient + √concurrency
concurrency = clamp(round(newLimit), min, max)

| Situation | gradient | What happens | | --- | --- | --- | | No contention (latency ≈ baseline) | ≈ 1 | Concurrency grows by √n | | Moderate contention (latency 2× baseline) | ≈ 0.5 | Concurrency roughly halves, plus small √n bump | | Heavy contention (latency 10× baseline) | ≈ 0.1 | Concurrency drops sharply |

The minLatency baseline slowly decays toward the EMA so that a single anomalously-fast early task doesn't permanently skew the gradient. Genuinely fast tasks will continuously refresh the baseline.

FAQ

Do I need to pick a concurrency number?

No. The defaults (initialConcurrency: 4, maxConcurrency: 64) work for most workloads. Sway will find the right level automatically. You can set maxConcurrency as a safety cap if your downstream has a known hard limit.

How many tasks does sway need to "warm up"?

The controller starts adjusting after the first probeInterval completions (default: 8 tasks). Within 2-3 probe windows it's usually near optimal. For very small batches (< 20 tasks), the overhead of learning may not pay off -- consider a fixed pool instead.

Can sway make things slower than a fixed pool?

In benchmarks, sway runs within ~1.1-1.2x of the optimal fixed pool -- the one you'd pick if you already knew the perfect number. The small overhead comes from the learning phase. If you already know the right concurrency, a fixed pool will be marginally faster.

What happens on the first error?

Sway rejects immediately, just like Promise.all(). Remaining in-flight tasks are not cancelled (promises are not cancellable), but no new tasks are started.

Does the order of results match the input?

Yes. results[i] corresponds to tasks[i], regardless of completion order.

Can I use sway with generators or async iterables?

Sway accepts any Iterable (arrays, generators, Set, custom iterables). Tasks are consumed lazily. Note: AsyncIterable is not currently supported -- the iterator must be synchronous, but each task function returns a promise.

Why no sway.any or sway.race?

Promise.any and Promise.race are typically used with small alternative sets (2-5 mirrors, replicas, fallbacks). Sway's controller needs at least ~20 tasks to learn anything useful, and once one task settles the scheduler stops -- adaptive concurrency never gets to do its job. For race-to-first-success workloads you usually want full parallelism (fire everything at once); throttling literally delays the win. The native Promise.any / Promise.race are the right tools there.

Benchmarks

500 tasks, simulated back-pressure resource (source).

Contention resource (optimal = 8)

| Approach | Time | vs Optimal | | --- | ---: | ---: | | fixed pool (c=8) -- optimal | 687ms | 1.0x | | sway (defaults) | 802ms | 1.2x | | fixed pool (c=32) -- too high | 5,672ms | 8.3x | | fixed pool (c=64) -- way too high | 15,022ms | 21.9x |

Shifting capacity (8 to 4 mid-run)

| Approach | Time | vs Best | | --- | ---: | ---: | | fixed pool (c=6) -- best compromise | 1,413ms | 1.0x | | sway (defaults) | 1,693ms | 1.2x | | fixed pool (c=8) -- wrong after shift | 2,025ms | 1.4x |

Sway's ~1.2x overhead is the cost of learning. A wrong fixed guess costs 8-50x.

Settled-mode benchmarks (sway.allSettled)

500 tasks against the same contention resource (optimal = 8), with a fraction of tasks failing fast (~1ms pre-flight rejections). Ratios are vs. the best approach in each row:

| Failure rate | Promise.allSettled | Fixed c=8 | Fixed c=32 | sway.allSettled | | --- | ---: | ---: | ---: | ---: | | 10% (flaky) | 197x slower | 1.0x | 8.2x slower | 1.16x slower | | 50% (very flaky) | 97x slower | 1.0x | 7.8x slower | 1.09x slower | | 100% (fully cooked) | 1.0x | 23x slower | 6.9x slower | 44x slower ✱ |

Same takeaway as plain sway(): adaptive control lands within ~1.1x of the optimal fixed pool without being told what that optimum is. Any fixed choice loses at one end of the spectrum -- c=8 is great under contention and terrible without it; unbounded Promise.allSettled is the opposite. Sway adapts to whichever regime it finds itself in.

✱ At 100% failures the controller never receives a successful-task latency to learn from, so it stays at initialConcurrency (default 4). Cheap in absolute terms (~150ms for 500 fast-fail tasks) but not where adaptive shines. For a workload you know fails close to 100% of the time, use Promise.allSettled directly.

Reproduce with npm run benchmark (vitest bench). Resource models inspired by Netflix's concurrency-limits (blog post).

Advanced: AdaptiveController

The controller is exported separately if you want to build your own scheduling loop:

import { AdaptiveController } from '@icazemier/sway';

const controller = new AdaptiveController({ maxConcurrency: 32 });

controller.getConcurrency();       // current concurrency level
controller.recordCompletion(12.5); // signal a task completed in 12.5ms
controller.getStats(100, 5000);    // get telemetry snapshot

License

MIT