惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
CERT Recently Published Vulnerability Notes
U
Unit 42
T
The Blog of Author Tim Ferriss
H
Hackread – Cybersecurity News, Data Breaches, AI and More
B
Blog RSS Feed
Microsoft Azure Blog
Microsoft Azure Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Securelist
L
Lohrmann on Cybersecurity
Blog — PlanetScale
Blog — PlanetScale
Recorded Future
Recorded Future
D
DataBreaches.Net
Spread Privacy
Spread Privacy
T
Threat Research - Cisco Blogs
I
Intezer
P
Palo Alto Networks Blog
Simon Willison's Weblog
Simon Willison's Weblog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
I
InfoQ
宝玉的分享
宝玉的分享
Security Latest
Security Latest
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
Threatpost
Cisco Talos Blog
Cisco Talos Blog
P
Proofpoint News Feed
博客园 - 司徒正美
H
Hacker News: Front Page
Y
Y Combinator Blog
爱范儿
爱范儿
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
NISL@THU
NISL@THU
月光博客
月光博客
有赞技术团队
有赞技术团队
Cloudbric
Cloudbric
酷 壳 – CoolShell
酷 壳 – CoolShell
G
Google Developers Blog
A
Arctic Wolf
博客园 - 【当耐特】
W
WeLiveSecurity
V
Visual Studio Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
V
V2EX
C
Cyber Attacks, Cyber Crime and Cyber Security
S
SegmentFault 最新的问题
The GitHub Blog
The GitHub Blog
The Cloudflare Blog
Stack Overflow Blog
Stack Overflow Blog

Hacker News: Show HN

PurrrrrFocus: Pomodoro Timer App - App Store Workflow Engine — Multi-Step Orchestration for Bun RapidPhoto: Pro Photo Editor App - App Store GitHub - DheerG/swarms: Achieve extraordinary results with claude code across a variety of tasks SPICE simulation → oscilloscope → verification with Claude Code — Lucas Gerads Show HN: VCoding – A 5 MB native Windows IDE with no dynamic dependencies Show HN: LLMs don't hallucinate because they're bad at math, it's the format GitHub - Agent-FM/agentfm-core: AgentFM is a peer-to-peer network that turns everyday computers into a decentralized AI supercomputer. AgentFM lets you run massive AI workloads directly across a global mesh of idle CPUs and GPUs. Show HN: Tracking Top US Science Olympiad Alumni over Last 25 Years GitHub - Potarix/agent-hub: One place to talk to all your agents Show HN: Runtime security for AI agents(injection,tool abuse, data exfiltration) GitHub - dubeyKartikay/lazyspotify: Terminal Spotify client for macOS and Linux GitHub - the-banana-tool/king-louie: Easy to use GUI Personal AI Assistant. Win/Linux/Mac. Show HN I made my vacation rental bookable by AI agents–no Airbnb, 0% commission GitHub - basteez/jsf-autoreload: maven plugin to enable hot reload on jsf projects uvm32/hosts/host-gdbstub at main · ringtailsoftware/uvm32 GitHub - labsai/EDDI: Config-driven engine that turns JSON into production-grade AI agents. Multi-agent orchestration, 12+ LLM providers, MCP/A2A protocols, RAG, persistent memory, and enterprise compliance (EU AI Act, GDPR, HIPAA). Built on Quarkus. GitHub - glitchnsec/fortyone-oss: AI Executive Assistant Platform Quickstart | Alien GitHub - muxshed/shed: One stream in, or many. Every destination, simultaneously. No cloud middleman, no per-channel fees, no limits. GitHub - ocrbase-hq/ocrbase: 📄 PDF/IMG ->.MD/JSON Document OCR API for PaddleOCR and GLMOCR. Self-hostable. GitHub - impactjo/home-memory: MCP server that lets your AI assistant remember everything about your home. GitHub - Sets88/dbcls: DbCls is a powerful terminal database client that supports various databases GitHub - neptun2000/heor-agent-mcp GitHub - SeanFDZ/macmind: Single-layer transformer in HyperTalk for the classic Macintosh RollQuation: Math Puzzles - Apps on Google Play GitHub - dropbox/witchcraft Show HN: Agent-cache – Multi-tier LLM/tool/session caching for Valkey and Redis GitHub - opentalon/opentalon: OpenTalon is an open-source platform built from the ground up in Go as a robust alternative to OpenClaw LinkedIn™ 职位抓取工具 - Chrome 应用商店 GitHub - EdoardoBambini/Agent-Armor-Iaga: AI agents are getting tool access — shell, file system, databases, APIs, secrets. But **nobody is governing what they actually do with it**. Frameworks like LangChain, CrewAI, AutoGen, and Claude Code give agents the power to execute. Agent Armor gives you the power to control, audit, and approve every single action before it happens. HN Vibes — Week 15, Apr 7–13 2026 GitHub - chojs23/ec: Easy terminal-native 3-way git mergetool vim-like workflow GitHub - SethPyle376/hiraeth: Local AWS emulator focused on fast integration testing, with SQS support, SQLite-backed state, and a debug-friendly web UI. GitHub - JakOb-dotcom/cloud-sandbox-security-analysis: Technical analysis and Proof of Concept (PoC) regarding environment variable exfiltration in containerized cloud sandboxes via side-channel data leaks. Springboards - Flint Alpha Show HN: A simpler coding agent harness GitHub - audiodude/sudomake-friends GitHub - 256thFission/mini-mythos: OSS clone of Anthropic’s Mythos harness to locate C/C++ memory vulnerabilities Show HN: OpenParallax: OS-level privilege separation for AI agent execution Hacker News Sorted - Chrome 应用商店 Show HN: How to Install Docker on Ubuntu 24.04 LTS: Complete 2026 Guide GitHub - himanshudongre/smriti GitHub - sverrirsig/claude-control: macOS desktop dashboard for monitoring and managing multiple Claude Code sessions GitHub - ory/dockertest: Write better integration tests! Dockertest helps you boot up ephermal docker images for your Go tests with minimal work. Chiral - Chrome 应用商店 Show HN: Two Claudes collaborating through shared memory on a $100 mini-PC GitHub - pmichaillat/latex-cv: Minimalist LaTeX template for academic CVs GitHub - oguzbilgic/posse: A web UI for Anthropic Managed Agents. GitHub - sshiraz/depsly: Dependency risk analysis tool for npm packages ABI Add safari/agent-harness — Safari browser automation via safari-mcp by achiya-automation · Pull Request #212 · HKUDS/CLI-Anything GitHub - Halfblood-Prince/trustcheck: Verify PyPI package attestations and improve Python supply-chain security GitHub - oguzbilgic/kern-ai: Agents that do the work and show it. GitHub - bruits/satteri: High-performance Markdown and MDX processing for the JavaScript ecosystem GitHub - tylergibbs1/feedstock: High-performance web crawler and scraper for TypeScript, powered by Bun and Playwright GitHub - Grimm67123/grimmbot: The self-improving sandboxed and open-source AI agent. With persistent memory and scheduling. GitHub - whitevanillaskies/whitebloom: Local whiteboard that blooms. GitHub - hwdsl2/docker-whisper: Docker image for a self-hosted Whisper speech-to-text server with speaker diarization and OpenAI-compatible transcription and translation APIs. Powered by faster-whisper. Supports all Whisper models, NVIDIA GPU (CUDA) acceleration, JSON/SRT/VTT output, SSE streaming, offline mode, and multi-arch (amd64, arm64). GitHub - yisding/reviewwiggum GitHub - MarwanAlsoltany/serrors: Structured errors for Go: sentinel hierarchies, typed data, custom formatting, and slog integration. GitHub - soatok/age-php GitHub - Luthiraa/markitme GitHub - stagas/rtdiff: realtime git diff gui and AI-assisted commits GitHub - tombedor/excalicharts GitHub - wh1le/excalidraw-edit: Open and edit .excalidraw files from the terminal. Offline, auto-saves to disk. MalExt Sentry - Malicious Extension Scanner - Chrome 应用商店 GitHub - syi0808/asciianimesvg: Generate animated ASCII art SVGs from text. CLI, Rust library, WASM, and web editor. GitHub - zaina-ml/ml_forge: A visual-based graph node editor for training computer vision models. GitHub - anakin87/llm-rl-environments-lil-course: 🌱 A little course on Reinforcement Learning Environments for evaluating and training Language Models GitHub - takaakit/superpowers-uml: Superpowers-UML modifies Superpowers to ensure a software development workflow in which AI agents design through UML modeling. AdriByte Studio - Sviluppo Web e Soluzioni Digitali GitHub - chouligi/angel-copilot: Your personalized Angel Investment Advisor Show HN: MoodSense AI (ML and FastAPI and Gradio, Deployed on Hugging Face) Moodsense Ai - a Hugging Face Space by aman179102 GitHub - agenteractai/lodmem: Level Of Detail Context Management for Agents GitHub - ostefani/subnetlens: A fast, concurrent network scanner with a TUI and plain-text CLI, built in Go. It discovers live hosts on your network, scans their open ports, resolves hostnames, and fingerprints operating systems—delivered. Cyber Pulse: Agentic Intel - Apps on Google Play Whisper API: Self-Hostable Speech to Text Transcription The Agent-Web Protocol Stack: A Research Thesis GitHub - msmarkgu/RelayFreeLLM: A restful API designed to route user prompts to various AI model providers. Show HN: Provepy – A Python decorator that proves your code using Lean and LLMs Show HN: Pardonned.com – A searchable database of US Pardons GitHub - patrickdappollonio/dux: Dux is a terminal UI that lets you run multiple AI coding agents side by side, each in its own git worktree, with full companion terminals, macros, commit generation, and a command palette that knows more tricks than you do. kMC Crystal Simulator Show HN: HyperFlow – A self-improving agent framework built on LangGraph GitHub - stef41/vibescore: 🎵 Grade your vibe-coded project. One command, instant letter grade across security, quality, dependencies, and testing. GitHub - stef41/lmscan: 🔍 Detect AI-generated text and fingerprint which LLM wrote it. Open-source GPTZero alternative. Zero dependencies, works offline. imgur.com GitHub - visionscaper/collabmem: Enabling long-term collaboration with Agentic AI - building up episodic and world model memory over time with in-context awareness 在 Steam 上购买 FriedrichAI: Offline AI 立省 10% GitHub - atripati/ark: AI Runtime Kernel — a context operating system for AI agents. Eliminates tool bloat, loads only what’s needed, and gives LLMs their reasoning space back. GitHub - nowork-studio/toprank: Open-source Claude Code skills for SEO, SEM, Google Ads GitHub - tacomanator/sash: Lightweight macOS menu bar app for reliably cycling through windows of the current application. Appents | Social Media Management for Product-First Teams GitHub - pnhoang/youtube-spam-blocker: Automatically detects and hides spam messages in YouTube Live chat. Set rate limits, keyword filters, and block repeat offenders. GitHub - decisionnode/DecisionNode: CLI + Local MCP - A shared structured memory store across Claude Code, Cursor, Windsurf, Antigravity, and every MCP client. Semantically queryable. GitHub - AvaCodeSolutions/django-email-learning: An open source Django app for creating email-based learning platforms with IMAP integration and React frontend components. The $100K Gap in Kubernetes Security Tooling Function Calling Harness: From 6.75% to 100%
GitHub - greenstick/regulo: Control the heat. A concurrency limiter with built-in priority queue, circuit breaker, adaptive backoff, and windowed metrics.
bnjemian · 2026-06-26 · via Hacker News: Show HN

🔥 Control the heat

npm version CI coverage minzipped size types license Socket


A concurrency limiter with a built-in circuit breaker, so the expensive parts of your system never boil over. Regulo is a priority-queue semaphore with weighted permits, a saturation circuit breaker, adaptive backoff, and built-in windowed metrics. Zero dependencies, ships ESM and CJS, runs on Node.js and other modern JavaScript runtimes.

Like the dial on a gas range, Regulo sits between incoming work and the burner. Most concurrency libraries just cap how many things run at once and stop there. Regulo is built for the case where that limit is protecting something expensive — SSR rendering, a database pool, a downstream API — and you need to watch the flame, send the important pots to the front, and turn things down cleanly before the system scorches.

✨ Highlights

  • 🎛️ Bounded concurrency, with priority and weighting — set how many burners are lit, send important work to the front, and let one heavy job claim more than one burner.
  • 🛡️ Saturation circuit breaker — when work backs up faster than it clears, Regulo takes the pot off the heat: it opens the circuit and sheds load immediately, then probes for recovery and closes again on its own. (See How the circuit breaker works — it trips on saturation, not on your operation's errors.)
  • 🌡️ Adaptive backoff — during a timeout burst, dispatch eases down to a simmer and returns to a full boil on its own once things recover.
  • 📈 Built-in observability — windowed 1m/5m/15m/1h/24h rollups (throughput, latency, queue depth, in-flight), lifetime counters, and an event stream, all through one status() call.
  • ⏳ Head-of-line fairness — once a caller is in line, nobody jumps the queue ahead of it.
  • 🪶 Tiny footprint, no supply-chain surface — roughly 6.6 KB min+gzip (~26 KB minified, ~6.1 KB brotli) with zero runtime dependencies, so there's nothing transitive to audit, update, or trust. Tree-shakeable ESM.
  • 🧯 Production-minded — graceful drain(), reset(), cancel(), and shutdown(); stale-task purging; double-release safety; strict-mode TypeScript types.

📦 Install

Requires Node.js >= 20 (or any runtime providing AbortSignal, queueMicrotask, and timers).

🚀 Quick start

import { Semaphore } from 'regulo';

const semaphore = new Semaphore(10); // 10 concurrent permits — ten burners

const result = await semaphore.use(async () => {
  return await expensiveOperation();
});

use() acquires a permit, runs your function, and releases the permit afterward — even if the function throws. The primary export is the Semaphore class; regulo is the dial wrapped around it.

⚖️ How it compares

Regulo overlaps with several well-known libraries but sits at the intersection of bounded concurrency, prioritization, and resilience, with built-in observability.

Capability regulo p-limit p-queue opossum cockatiel
Bounded concurrency Yes Yes Yes No Yes (bulkhead)
Priority queue Yes No Yes No
Weighted permits Yes No No No
Circuit breaker Yes No No Yes Yes
Adaptive backoff Yes No No No No
Windowed metrics Yes No Basic Yes No
Dependencies Zero Minimal Minimal Several Zero

Capabilities reflect each project's commonly documented feature set at the time of writing; check the respective projects for their current state. If you only need a concurrency cap, p-limit is smaller and simpler. If you need rich resilience policy composition (retry, timeout, fallback), cockatiel is a strong choice. Reach for regulo when you want prioritized, weighted concurrency limiting that you can monitor and that protects itself under sustained load.

🎯 Core concepts

Semaphore — holds a fixed pool of permits (the burners). Callers acquire a permit before doing work and release it when done. When all permits are held, callers queue until one frees up, or until their timeout fires.

Weighted permits — a single acquire can consume more than one permit (weight). Big pots need more burners, so heavier work reserves proportionally more of the pool.

Priority queue — queued callers are dispatched in ascending priority order (lower number = higher priority — the front burner). Default priority is 0. Dispatch is head-of-line fair: once any caller is queued, later callers always queue behind it rather than grabbing a free permit, so a lower-priority or lighter task can never jump ahead of a waiting higher-priority or heavier one.

Queue orderingqueueOrder selects a dispatch preset. Priority and arrival order are two independent axes. 'fifoWithPriority' (the default) and 'lifoWithPriority' keep priority as the primary sort key and break equal-priority ties earliest- or latest-enqueued first. 'fifo' and 'lifo' drop priority entirely and order purely by enqueue time. For full control, pass a comparator (lower sorts/dispatches first), which overrides queueOrder:

import { Semaphore, QUEUE_ORDERINGS } from 'regulo';

// Built-in preset — pure arrival order, priority ignored
const a = new Semaphore(4, { queueOrder: 'lifo' });

// Custom: priority first, then lightest work first, then FIFO. The receiver is a
// read-only QueuedTaskView ({ id, priority, enqueueTime, weight }); `id`
// increases with enqueue order. Probe tasks are always dispatched first
// regardless of your comparator, so you never need to handle them.
const b = new Semaphore(4, {
  comparator: (x, y) => (x.priority - y.priority) || (x.weight - y.weight) || (x.id - y.id),
});

// Compose with a preset
const c = new Semaphore(4, { comparator: QUEUE_ORDERINGS.lifoWithPriority });

See Choosing an ordering for how the ordering interacts with weighted permits and head-of-line dispatch — the choice has real throughput consequences under mixed weights.

Circuit breaker — watches for saturation and takes the pot off the heat when the system can't keep up. See the dedicated section below for exactly what it measures.

Backoff — exponential backoff eases dispatch down to a simmer during sustained timeout bursts. The delay grows on each timeout and decays continuously over time, throttling dispatch while downstream systems recover and returning to zero on its own once the burst subsides. The current delay is exposed in status() and TASKTIMEOUT events.

🧭 Choosing an ordering, and its implications

Two facts are true of every ordering, because they live in the scheduler, not the comparator:

  1. Dispatch follows the configured order strictly. Whatever sorts to the head dispatches next; nothing behind it jumps ahead (head-of-line fairness).
  2. The scheduler will not dispatch past a head that doesn't fit. With weighted permits, if the head needs more permits than are currently free, the scheduler waits for capacity to accumulate rather than skipping to a lighter task behind it. A free permit can therefore sit idle while the head waits. This prevents a lighter/lower-priority task from starving a heavier/higher-priority one — but under a wide weight distribution it can also stall throughput while the head waits.

That second rule is where the comparator choice matters: the rule is fixed, but which task ends up at the head — and therefore how often the stall bites — is entirely up to your ordering.

queueOrder Dispatches first Head-of-line stall exposure under mixed weights
fifoWithPriority (default) lowest priority value, ties earliest-first A heavy task only holds the line while it is genuinely the highest-priority (or earliest equal-priority) waiter
lifoWithPriority lowest priority value, ties latest-first Same, but equal-priority ties favor the newest
fifo earliest enqueued (priority ignored) Classic head-of-line: whoever arrived first holds the line until it fits
lifo latest enqueued (priority ignored) The newest arrival holds the line until it fits
custom, heaviest-first heaviest weight Worst case — deliberately parks the heaviest task at the head, maximizing the stall
custom, lightest-first lightest weight Minimizes stalls — light work drains while capacity accumulates for heavy work

If you mix many light acquires with a few heavy ones and care about throughput, prefer a lightest-first tiebreaker (e.g. (a, b) => (a.priority - b.priority) || (a.weight - b.weight) || (a.id - b.id)), or give each weight class its own Semaphore so a heavy head in one pool can't stall the other. Conversely, if you must not let light work starve heavy work, the default's strict behavior is what you want.

Weight is a cost multiplier, not a task-type selector. weight exists to say "this unit of work costs N burners," for work drawing on the same resource pool and same failure domain. Don't use weight (or priority) to multiplex unrelated task types or downstreams through one Semaphore — the breaker and backoff are shared across everything in the instance (see Caveats). Use one Semaphore per resource instead.

💡 How the circuit breaker works

The breaker is a saturation breaker, not a fault breaker. It watches the rate of queue-acquisition timeouts — callers that waited longer than queueMaxTimeout for a permit — over a sliding window. When that rate crosses circuitBreakerThreshold (and the minimum count guards are met), the circuit opens and new requests are rejected immediately with CIRCUIT_OPEN. After the cooldown elapses, one probe request is allowed through; if it succeeds the circuit closes, if it times out the circuit re-opens and the cooldown restarts.

What this means in practice:

  • The breaker trips when work backs up faster than permits free — the signature of a saturated or slow downstream. This is what protects the pool: it pulls everything off the heat before the pot boils over.
  • The breaker does not trip on errors thrown by the function you run inside use(). If your operation fails fast, the permit is released normally and the failure never reaches the breaker.

If you also need to trip on downstream errors (not just saturation), pair the semaphore with a conventional fault breaker around your operation, or use the standalone CircuitBreaker and feed it your own failure signal.

🥘 Recipe: Express middleware

Cap concurrent handling of an expensive route and shed load with a 503 when the circuit is open or the queue is full:

import { Semaphore, SemaphoreError, SemaphoreEvents } from 'regulo';
import type { RequestHandler } from 'express';

/*
Middleware
*/

export function limit(semaphore: Semaphore): RequestHandler {
  return async (req, res, next) => {
    let release: (() => void) | undefined;
    try {
      release = await semaphore.acquire();
    } catch (error) {
      // CIRCUIT_OPEN | QUEUE_FULL | TIMEOUT all mean the same to a client:
      // we're overloaded, come back later. No need to branch on error.code.
      if (error instanceof SemaphoreError) {
        res.setHeader('Retry-After', '5').sendStatus(503);
        return;
      }
      return next(error);
    }
    // Hold the permit for the whole request; release however the response ends
    // (success, error, or client disconnect). Regulo's release is idempotent.
    res.once('close', release);
    next();
  };
}

/*
Usage
*/

const reports = new Semaphore(20, { queueMaxLength: 100, queueMaxTimeout: 2000 });

app.get('/report', limit(reports), async (req, res) => {
  res.json(await buildExpensiveReport(req.query));
});

/*
Metrics
*/

// Expose the limiter's state to your metrics endpoint.
app.get('/metrics/semaphore', (_req, res) => res.json(reports.status()));

/*
Event Hooks
*/

// Events fire once per state change for the whole limiter — the right place
// for logging / metrics / alerting, never for responding to a single request.
reports.on(SemaphoreEvents.CIRCUITOPEN, ({ timeoutRate }) => logger.warn(`reports limiter shedding load (timeout rate ${(timeoutRate * 100).toFixed(0)}%)`));
reports.on(SemaphoreEvents.CIRCUITCLOSE, () => logger.info('reports limiter recovered'));

📚 API reference

new Semaphore(count, config?)

Creates a semaphore with count permits.

acquire(abortSignal?, priority?, weight?): Promise<() => void>

Acquires a permit. Returns a release closure. Queues if no permit is available.

  • priority — Dispatch priority (any finite number; lower dispatches first). Defaults to 0. Non-finite values (NaN, Infinity) reject with INVALID_PRIORITY.
  • weight — Permits to consume (integer in 1..count). Defaults to 1. Invalid weights reject with INVALID_WEIGHT.
const release = await semaphore.acquire(abortController.signal, 1, 2); // priority 1, weight 2
try {
  await doWork();
} finally {
  release();
}

use<T>(fn, abortSignal?, priority?, weight?): Promise<T>

Preferred entry point. Acquires a permit, runs fn(), and releases — always, even if fn throws.

tryAcquire(weight?): (() => void) | null

Non-blocking. Returns a release closure, or null if insufficient permits are available or any tasks are already queued (head-of-line fairness — tryAcquire never jumps the queue).

  • weight — Permits to consume (integer in 1..count). Defaults to 1. Invalid weights return null.

drain(timeoutMs?): Promise<void>

Resolves when the queue is empty and all permits are returned. Multiple callers share the same promise. Pass timeoutMs (a positive integer) to set a deadline — rejects with TIMEOUT if not idle in time; an invalid value throws synchronously.

Without timeoutMs, drain() can block indefinitely if a caller holds a permit and never releases it.

reset(options?): void

Rejects all queued tasks (SHUTDOWN) and restores the semaphore to its initial state. Event listeners are preserved unless { clearListeners: true } is passed.

cancel(): void

Rejects all currently queued tasks with CANCELLED. In-flight permits are unaffected and the semaphore remains fully operational (unlike shutdown()).

shutdown(reason?): void

Permanently stops the semaphore — kills the gas. All queued tasks are rejected. Unlike reset(), this cannot be reversed.

on(event, listener) / off(event, listener) / removeAllListeners(event?)

Standard event emitter interface. See Events reference below.

status()

Returns a snapshot of current operating state. See status() output for the full shape.

status() is O(1) in queue depth — safe to call on a metrics scrape path. (Queue age is read from an enqueue-ordered index, not by scanning the queue.)

isAvailable(): boolean

Returns true if the semaphore is not shut down, the circuit is not open, and a permit is available.

queueLength: number

Current number of tasks waiting for a permit.

availablePermits: number

Number of permits not currently held.

⚙️ Configuration reference

Option Type Default Description
queueMaxLength number 1024 Max tasks that may wait in the queue; further acquires reject with QUEUE_FULL. Positive integer; pass Number.MAX_SAFE_INTEGER for an effectively unbounded queue
queueMaxTimeout number 10000 ms a queued task waits before TIMEOUT
queueMaxAge number 30000 ms before the purge interval ejects a task regardless of its own timeout
rejectOnFull boolean false Reject immediately when all permits are held (no queuing)
circuitBreakerThreshold number 0.5 Failure rate in (0,1) that trips the circuit
circuitBreakerWindow number 10000 Sliding window size in ms for the failure rate. Min: 1000
circuitBreakerCooldown number 5000 ms the circuit stays open before allowing a probe. Min: 1000
circuitBreakerMinThroughput number 10 Min requests in window before circuit can trip
circuitBreakerMinFailures number 5 Min failures in window before circuit can trip
backoffInitialTimeout number 50 Initial backoff delay (ms) applied to scheduler wakeup on first timeout
backoffMaxTimeout number 2000 Max backoff delay (ms) applied to scheduler wakeup
backoffDecayFactor number 0.5 Backoff decay factor per idle second, in (0,1)
purgeIntervalMs number 3000 ms between stale-task purge sweeps. Min: 500
metricsEnabled boolean true Enable windowed metrics collection
queueOrder 'fifo' | 'lifo' | 'fifoWithPriority' | 'lifoWithPriority' 'fifoWithPriority' Queue dispatch order. fifo/lifo order purely by enqueue time; the *WithPriority variants make priority primary and break ties by enqueue time. See Choosing an ordering. Ignored if comparator is set
comparator (a, b) => number Custom ordering over queued tasks (lower sorts/dispatches first); overrides queueOrder
debug boolean false Enable debug logging and the permit-pool invariant check. Does not gate events — all events fire regardless

Every option is optional. The object below is the complete set of defaults — copy it and change only what you need:

import { Semaphore, type SemaphoreConfig } from 'regulo';

const config: SemaphoreConfig = {
  // Queue
  queueMaxLength: 1024,                    // max waiting tasks before QUEUE_FULL; min 1
  queueMaxTimeout: 10000,                  // ms a queued task waits before TIMEOUT; min 1
  queueMaxAge: 30000,                      // ms before the purge sweep ejects a task; min 1
  rejectOnFull: false,                     // true = no queuing; reject when all permits held
  // Circuit breaker
  circuitBreakerThreshold: 0.5,            // timeout rate in (0,1) that trips the circuit
  circuitBreakerWindow: 10000,             // ms sliding window for the rate; min 1000
  circuitBreakerCooldown: 5000,            // ms open before a probe is allowed; min 1000
  circuitBreakerMinThroughput: 10,         // min requests in window before it can trip; min 1
  circuitBreakerMinFailures: 5,            // min failures in window before it can trip; min 1
  // Adaptive backoff
  backoffInitialTimeout: 50,               // ms initial delay on first timeout in a burst
  backoffMaxTimeout: 2000,                 // ms ceiling for the backoff delay
  backoffDecayFactor: 0.5,                 // decay per idle second, in (0,1)
  // Maintenance & observability
  purgeIntervalMs: 3000,                   // ms between stale-task purge sweeps; min 500
  metricsEnabled: true,                    // windowed metrics collection
  debug: false,                            // debug logging + permit-pool invariant check
  // Ordering
  queueOrder: 'fifoWithPriority',          // 'fifo' | 'lifo' | 'fifoWithPriority' | 'lifoWithPriority'
  // comparator: undefined,                // no default — if set, overrides queueOrder
};

const semaphore = new Semaphore(10, config);

⇄ Events reference

Listen with Semaphore.on(SemaphoreEvents.CIRCUITOPEN, handler).

Event constant String value Payload
TASKACQUIRE 'task-acquire' { queued, running, probe? }
TASKRELEASE 'task-release' { queued, running }
TASKTIMEOUT 'task-timeout' { queueLength, backoffDelay, taskId }
TASKABORT 'task-abort' none
QUEUEPURGE 'queue-purge' QueuedTask
CIRCUITOPEN 'circuit-open' { timeoutRate, recentTimeouts, total, reason? }
CIRCUITHALFOPEN 'circuit-half-open' none
CIRCUITCLOSE 'circuit-close' none
SHUTDOWN 'shutdown' reason: string

🚨 Error codes

All rejections are SemaphoreError instances with a code property.

Code When thrown
CIRCUIT_OPEN Circuit breaker is open
CIRCUIT_HALF_OPEN Circuit is half-open and a probe is already in flight
INVALID_WEIGHT weight is not an integer in 1..count
INVALID_PRIORITY priority is not a finite number
QUEUE_FULL rejectOnFull is true, or queueMaxLength is exceeded
TIMEOUT Task waited longer than queueMaxTimeout; or drain() exceeded its deadline
ABORTED Caller's AbortSignal fired
CANCELLED Task was rejected by cancel()
SHUTDOWN shutdown() or reset() was called while the task was queued
PURGED Task was ejected by the stale-task purge interval (queueMaxAge exceeded)
import { Semaphore, SemaphoreError } from 'regulo';

const semaphore = new Semaphore(10);
try {
  const release = await semaphore.acquire();
  // ...
  release();
} catch (error) {
  if (error instanceof SemaphoreError) {
    switch (error.code) {
      case 'CIRCUIT_OPEN':    // back off and retry later
      case 'TIMEOUT':         // shed load
      case 'ABORTED':         // client disconnected
    }
  }
}

status() output

{
  status: {
    running: number,           // permits currently held
    queued: number,            // tasks waiting in queue
    available: number,         // free permits
    inFlight: number,          // same as running (alias for clarity)
    pendingReleases: number,   // outstanding release closures; non-zero means permits are held
    circuitOpen: boolean,
    circuitHalfOpen: boolean,
    backoffDelay: number,      // current backoff delay (ms) applied to scheduler wakeup
    requestsPerSecond: number, // based on 1m window
    timeoutRate1m: number,     // timeout % over last 1m
    queueAge: number,          // ms since oldest queued task was enqueued
  },
  lifetime: {
    totalAcquired: number,
    totalReleased: number,
    totalTimeouts: number,
    circuitBreakerCooldownRemaining: number, // ms until circuit may probe
  },
  metrics: SemaphoreMetricsSnapshot | null  // null if metricsEnabled: false
}

🔌 Standalone CircuitBreaker

CircuitBreaker can be used independently — e.g. to wrap an HTTP client. Unlike the semaphore's saturation breaker, here you decide what counts as a failure by calling recordTimeout() on whatever signal you choose:

import { CircuitBreaker } from 'regulo';

const circuitBreaker = new CircuitBreaker({
  threshold: 0.5,
  window: 10000,
  cooldown: 5000,
  minThroughput: 10,
  minFailures: 5,
});

async function fetch(url: string) {
  // Check and transition open → half-open if cooldown elapsed
  if (circuitBreaker.checkAndTransition()) {
    console.log('Circuit entering half-open');
  }
  if (circuitBreaker.isOpen) throw new Error(`Circuit open, retry in ${circuitBreaker.cooldownRemaining}ms`);

  circuitBreaker.trackAttempt();
  try {
    const result = await httpClient.get(url);
    if (circuitBreaker.isHalfOpen) circuitBreaker.handleProbeSuccess();
    return result;
  } catch (error) {
    circuitBreaker.recordTimeout();
    if (circuitBreaker.isHalfOpen) circuitBreaker.handleProbeFailure();
    else circuitBreaker.evaluateAndTrip();
    throw error;
  }
}

⚡ Benchmarks

Full, reproducible benchmarks live in benchmarks/ — run them yourself with npm run benchmark:all. Figures below are from a real run on Node v22.16.0, darwin x64, mid-2018 Intel i9 Macbook Pro; your numbers will differ — re-run locally. Each library from How it compares is benchmarked only on the axis it actually shares with Regulo: the concurrency limiters on capping concurrency, the circuit breakers on per-call overhead.

🔥 Fast path, uncontended

Scenario ops/sec vs. fastest
tryAcquire + release 1.96M 2.40x slower
tryAcquire + release (no metrics) 4.71M fastest
use() round-trip 1.03M 4.55x slower
use() round-trip (no metrics) 1.49M 3.15x slower

🎛️ Weighted acquire, uncontended

Scenario ops/sec vs. fastest
use() weight=1 1.05M fastest
use() weight=4 1.03M 1.02x slower
use() weight=16 1.00M 1.05x slower

Weighted permits add no meaningful overhead regardless of weight — claiming 16 burners at once costs about the same as claiming one.

⏳ Contended throughput (tasks/sec)

Scenario tasks/sec vs. fastest
concurrency=4 647.5k 1.05x slower
concurrency=16 669.5k 1.01x slower
concurrency=64 679.0k fastest
concurrency=16, random priority 597.2k 1.14x slower

📈 status() snapshot cost

Queue depth ops/sec vs. fastest
0 675.9k 1.02x slower
100 686.9k fastest
1000 665.9k 1.03x slower

status() is O(1) in queue depth — the cost is flat across queue depths (within run-to-run noise) because queue age is read from an enqueue-ordered index rather than by cloning and scanning the queue. status() is safe to call on a metrics scrape path for arbitrarily long task queues.

📊 regulo vs. other libraries — uncontended round-trip

Library ops/sec vs. fastest
cockatiel (bulkhead) 3.86M fastest
p-queue 1.10M 3.50x slower
p-limit 1.09M 3.55x slower
regulo (no metrics) 1.46M 2.65x slower
regulo 927.7k 4.16x slower

📊 regulo vs. other libraries — contended throughput @ concurrency=16 (tasks/sec)

Library tasks/sec vs. fastest
cockatiel (bulkhead) 1.64M fastest
p-queue 975.6k 1.68x slower
regulo (no metrics) 884.1k 1.85x slower
p-limit 868.1k 1.89x slower
regulo 670.6k 2.45x slower

🛡️ Circuit breaker overhead — closed/healthy circuit

Library ops/sec vs. fastest
regulo CircuitBreaker 3.29M fastest
cockatiel (circuitBreaker) 2.42M 1.36x slower
opossum 1.46M 2.26x slower

The picture is consistent. Cockatiel's bulkhead is the fastest limiter — and Regulo trades raw limiter throughput for an integrated priority heap, weighted permits, a saturation breaker, and (by default) windowed metrics in one component. With metrics disabled its contended throughput sits right alongside p-limit and p-queue, so most of the remaining gap to a bare limiter is the windowed metrics you can turn off. On the breaker axis the integration goes the other way: Regulo's standalone CircuitBreaker is the fastest of the three, because it defers failure accounting to an explicit timeout signal instead of bookkeeping a rolling window on every call.

In practice none of this is the bottleneck. Regulo guards work that is far more expensive than the limiter itself: SSR renders, database queries, downstream API calls, measured in milliseconds. Even at ~670k tasks/sec under contention the per-task overhead is a few microseconds against operations thousands of times slower. If you only need a bare concurrency cap on cheap work in a hot loop, reach for a leaner limiter; see How it compares.

🧪 Test coverage

npx vitest run --coverage
 ✓ test/queue.test.ts (7 tests)
 ✓ test/metrics.test.ts (18 tests)
 ✓ test/breaker.test.ts (21 tests)
 ✓ test/permit.test.ts (14 tests)
 ✓ test/backoff.test.ts (6 tests)
 ✓ test/ordering.test.ts (13 tests)
 ✓ test/heap.test.ts (9 tests)
 ✓ test/list.test.ts (8 tests)
 ✓ test/semaphore.test.ts (93 tests)

 Test Files  9 passed (9)
      Tests  189 passed (189)

⚠️ Caveats

Before you crank the dial, know where the edges are:

  • One Semaphore is one failure domain. The circuit breaker and adaptive backoff are per-instance and shared across everything routed through it, so a saturation event on one dependency trips the breaker for all work in that instance. Don't multiplex unrelated downstreams or task types through a single Semaphore (and don't use weight/priority to fake it) — use one Semaphore per protected resource, or one capacity pool plus a standalone CircuitBreaker per downstream key. See Choosing an ordering.
  • A free permit can sit idle behind a heavier head. The scheduler never dispatches past a head that doesn't fit, so under weighted permits one heavy task at the head can stall throughput even when there's capacity for the lighter tasks behind it. This is by design (it stops light work starving heavy work); how often it bites depends on your ordering — see Choosing an ordering.
  • drain() without a timeout can block indefinitely if a permit holder never releases. Always pass timeoutMs in graceful-shutdown paths.
  • The circuit breaker is a saturation breaker. It trips on queue-acquisition timeouts, not on errors thrown by your operation. See How the circuit breaker works.

📜 License

MIT