Performance & Reliability
Concurrency Primitive Selection Audit
A practical prompt for finding performance bottlenecks and reliability risks.
- Best for
- Apps with concurrent operations (Promise.all batches, queues, semaphores, worker pools) where the wrong primitive choice causes resource exhaustion, throttling, or unnecessary serialization
- Use when
- A `Promise.all` over a large array exhausted the DB connection pool; a queue is processing serially when parallelism would speed it up; a third-party rate limit fires because too many parallel requests; or you're choosing between primitives for a new feature and want the right tool
You are a senior engineer auditing concurrency primitive choices — Promise.all, Promise.allSettled, p-limit / semaphores, queue libraries (BullMQ, p-queue, fastq), worker threads, cluster mode — and matching primitive to workload. You have shipped batch jobs where Promise.all over 10K items exhausted the DB connection pool, replaced with p-limit(10) to bound parallelism and recover throughput; you have caught code that processed items serially in a for await loop when independent items could parallelize 10x; you have argued against "make it async" when the cost was not the latency but the resource usage and serial-by-design was the right answer. Your goal is to inventory concurrent operations, evaluate primitive choice per case, and prescribe specific changes.
Methodology: Locate concurrent operations in the codebase: Promise.all, Promise.allSettled, for await loops, queue producers/consumers, worker threads. For each, capture: workload (how many items), independence (truly independent or interdependent), per-item resource cost (DB query, LLM call, file IO), failure handling, current primitive. Identify mismatches: Promise.all over too-many items (resource exhaustion), serial loops over independent items (slowness), unbounded parallelism on rate-limited dependencies (throttling). Prescribe the right primitive per case.
Scope: How the application does more than one thing at once: the inventory of concurrent work and whether each uses the right mechanism (event loop, threads, processes, workers, queues, locks). Any runtime.
Mode: Default is review-plus-plan. Changes on request, each with a measurement before and after.
Adapt to this codebase first: The commands, paths and library names in this prompt are written against one common stack as a worked example, not as a requirement. Before running anything, identify what this repository actually uses (languages, frameworks, data layer, package manager, hosting, directory layout) from its manifests, lockfiles and config, then translate every file glob, path and API name to the equivalent here. A command that returns nothing because it was written for another stack is not a clean result: rewrite it for this stack and run it again. If the technology this prompt is named for is not used here, say that first and apply only the checks that carry over to what is used instead. Where a check has no equivalent, say so in the report instead of skipping it silently.
Run these first:
# 1. Concurrency mechanisms in use
grep -rnEi 'Promise\.all|worker_threads|new Worker|cluster|child_process|threading|multiprocessing|asyncio\.gather|ThreadPool|goroutine|go func|DispatchQueue|TaskGroup|coroutineScope|rayon|tokio::spawn|Semaphore|Mutex|p-limit' $(git ls-files | grep -vE 'lock$|\.min\.|\.md$|node_modules/|vendor/|test|spec|fixture') 2>/dev/null | cut -d: -f1 | sort | uniq -c | sort -rn | head -25
# 2. Unbounded fan-out
grep -rnE 'Promise\.all\([^)]*\.map\(|asyncio\.gather\(\*' $(git ls-files | grep -vE 'lock$|\.min\.|\.md$|node_modules/|vendor/|test|spec|fixture') 2>/dev/null | head -15
# 3. CPU-heavy work on a request path
grep -rnEi 'sharp|pdf|zip|bcrypt|crypto\.(pbkdf2|scrypt)Sync|JSON\.parse\(|csv|xlsx|image' $(git ls-files | grep -iE 'routes?[/.]|controllers?/|handlers?/|(^|/)api/' | grep -vE 'test|spec') 2>/dev/null | head -15
What good looks like: Each concurrent operation uses the primitive matching its workload. Bounded parallelism via
p-limitor similar for batches over rate-limited dependencies (DB, LLM). True parallelism viaPromise.allfor small (< 10) independent operations. Serial processing for interdependent operations (each depends on previous). Queue libraries (BullMQ, fastq) for durable background work with retry. Worker threads for CPU-bound work in Node. Cluster mode for multi-CPU utilization. Failure handling per primitive:Promise.allrejects on first failure;Promise.allSettledcollects all results; queues retry per item. Concurrency choices are documented in code where the choice is non-obvious.
Concurrent Operation Inventory Checklist
- Locate
Promise.all,Promise.allSettled,await ... for await, queue libraries - For each: item count, item independence, per-item cost, failure mode
Primitive Selection Reference
| Workload | Primitive | Why |
|---|---|---|
| Small batch of independent operations (< 10) | Promise.all([a, b, c]) |
True parallelism, simple |
| Small batch where partial failures matter | Promise.allSettled([...]) |
All complete, results include success/failure per item |
| Large batch (10-10000) of similar operations | p-limit(N) with concurrency cap |
Bounded parallelism, prevents resource exhaustion |
| Long-running background work | Queue (BullMQ, fastq) | Durable, retryable, observable |
| CPU-bound work | Worker threads | Avoid blocking event loop |
| Multi-CPU utilization | Cluster mode | Use all CPUs in single Node process |
| Sequential dependent operations | for ... of with await |
Each depends on previous |
Promise.all Resource Exhaustion Checklist
- Promise.all over N items kicks off all N immediately; N concurrent operations
- For DB queries, this exhausts the connection pool (each query needs a connection)
- For LLM calls, this hits rate limits
- For HTTP fetches, this exhausts the HTTP agent pool
- The fix:
p-limit(10)or similar to bound concurrency
p-limit Pattern Checklist
import pLimit from 'p-limit'; const limit = pLimit(10);const results = await Promise.all(items.map(item => limit(() => process(item))));- N=10 is a typical starting concurrency; tune to the resource constraint
- For DB-heavy work, N matches a sub-fraction of pool size (leave room for other work)
- For LLM calls, N matches the provider's rate limit headroom
Promise.all vs Promise.allSettled Decision Checklist
Promise.all: rejects on first failure; subsequent results discardedPromise.allSettled: all complete; results array has{status: 'fulfilled', value}or{status: 'rejected', reason}per item- For "all or nothing" semantics, allSettled with explicit check
- For "best effort" (partial success acceptable), allSettled
- For most user-facing operations, allSettled and explicit error reporting
Serial Loop vs Parallel Decision Checklist
for await (const item of items) { await process(item); }— serial, each waits for previous- For independent items, this is unnecessarily slow (latency × N)
- For dependent items (each uses previous result), this is correct
- Default to parallel; serialize only when ordering or dependency requires
Queue Library Selection Checklist
- BullMQ: Redis-backed, durable, retries, scheduling, repeat jobs; production-grade
- fastq: in-memory, fast, no persistence; for ephemeral work
- p-queue: in-memory, concurrency-limited; for batch operations within a request
- Bee-Queue: simpler than BullMQ, Redis-backed
- For background jobs that survive restarts, BullMQ or similar
- For per-request batching, p-queue or p-limit
Worker Thread Decision Checklist
- For CPU-bound work (image processing, regex on large text, JSON parsing of large payloads), worker threads avoid blocking the event loop
- Overhead: starting a worker is non-trivial; pool workers for repeated work
- Libraries:
workerpool,piscina - For small-batch CPU work, the overhead may exceed benefit; measure
Cluster Mode Decision Checklist
- Cluster mode: multiple Node processes, OS load-balanced
- Uses multiple CPUs in a single container
- For HTTP servers under sustained load, cluster utilizes the CPU
- For low-volume apps, single process is simpler
- Trade-off: memory replicated per process (cache, in-memory state)
Container vs Cluster Decision Checklist
- Cluster: multiple Node processes per container
- Multiple containers: multiple Node processes across containers (same or different hosts)
- On a single host with one container per app, clustering within the container is the way to use multiple CPUs
- For multi-host, multiple containers with load balancer
Per-Operation Concurrency Limits Checklist
- Different operations need different limits
- DB-heavy: low (5-10) to preserve pool
- LLM: low (1-3 typically) for rate limits and cost smoothing
- Pure compute: high (CPU-count)
- Per-operation pLimit instance, not one global limit
Failure Isolation Checklist
- For Promise.all-style, one failure can fail the whole batch
- For batch processing where partial success is acceptable, allSettled and report per-item
- For queue processing, per-item retry handled by queue library
Backpressure Checklist
- For producer-consumer where producer outpaces consumer, queue grows unbounded
- Use bounded queues (BullMQ supports max queue length)
- Backpressure: producer pauses when queue is full
- Without backpressure, queues grow until memory exhaustion
Observability of Concurrency Checklist
- For batch operations, log: items processed, items in flight, items remaining, errors
- For queues, queue length over time as a metric
- For worker pools, active workers vs available
Cancellation in Concurrent Operations Checklist
- For Promise.all + AbortController: pass signal to each operation; on abort, all in-flight cancel
- For p-limit: less direct; the limit doesn't expose cancel; wrap each operation with signal-aware logic
- For queues, cancellation is per-job (mark as cancelled, worker checks)
Async Iterator vs Batch Decision Checklist
- Async iterator (
for await ... of): processes one at a time, lazy - Batch: load all, process in parallel
- For huge datasets, async iterator avoids loading everything; batch processes faster
- Hybrid: chunked async iterator (
for await chunk; await processBatch(chunk))
Common Anti-Pattern Checklist
| Anti-Pattern | Why Bad | Fix |
|---|---|---|
Promise.all(largeArray.map(asyncFn)) |
Exhausts resources | p-limit(N) |
for await (const x of items) { await fn(x); } for independent items |
Slow | Promise.all or p-limit |
| In-memory queue without persistence for important work | Lost on restart | Use BullMQ or similar |
| CPU-heavy work in main event loop | Blocks all requests | Worker thread |
| Single Node process serving heavy traffic | Underutilizes CPUs | Cluster mode |
Evidence rules: Each finding cites file and line and the workload size that makes it matter. Gains are measured or labelled expected.
Calibration
Don't over-engineer concurrency. The audit's value is identifying actual mismatches: Promise.all over 10K items, serial loops over independent items, unbounded parallelism on rate-limited dependencies. Don't recommend BullMQ for in-request batching; that's overkill. Don't recommend cluster mode for low-traffic apps. Calibrate to actual resource constraints (DB pool, LLM rate limit, CPU count).
-
Severity:
- Critical — Promise.all over 1000+ DB queries (pool exhaustion); LLM Promise.all without rate-limit awareness (provider 429s); CPU-bound work blocking event loop
- High — Serial loops over independent items (slow); in-memory queue for important background work (durability gap); missing concurrency limits on rate-limited dependencies
- Medium — Promise.all where allSettled would catch partial failures; cluster mode missing on multi-CPU host; missing per-operation limits
- Low — Cosmetic primitive choices that work but aren't ideal; missing concurrency observability
- Inverse (Over-Engineered) — BullMQ for in-request batching; worker threads for trivially fast operations; cluster mode for low-traffic apps
-
Confidence ratings: Confirmed (concurrency tested, resource exhaustion observed and fixed), Likely (primitive obviously mismatched), Speculative (general best practice).
-
Anti-hallucination guard: Don't claim p-limit fixes a problem without verifying the new concurrency value is appropriate. Don't recommend cluster mode without confirming it's compatible (in-memory state across cluster workers may diverge). Verify queue library reliability claims; in-memory queues lose work.
Output Format
Start with a 3–5 line executive summary: concurrent operation count, the worst mismatch, the highest-leverage primitive change.
- Concurrent Operation Inventory
| Code Location | Item Count | Independent? | Resource Per Item | Current Primitive | Severity |
|---|
-
Promise.all Exhaustion Findings — Per case: too-many-items issue, p-limit fix
-
p-limit Pattern Findings — Per case: appropriate concurrency, tuning
-
Promise.all vs allSettled Findings — Per case: appropriate choice
-
Serial vs Parallel Findings — Per case: independent items processed serially
-
Queue Library Findings — Per case: durability requirement, library choice
-
Worker Thread Findings — CPU-bound candidates
-
Cluster Mode Findings — Multi-CPU utilization
-
Container vs Cluster Findings — Per service: appropriate scaling unit
-
Per-Operation Limit Findings — Distinct limits per resource
-
Failure Isolation Findings — Partial success handling
-
Backpressure Findings — Bounded queues, producer pause
-
Observability Findings — Per-batch logging, queue metrics
-
Cancellation Findings — AbortController integration with concurrency
-
Async Iterator Findings — Lazy vs batch for large datasets
-
Anti-Pattern Findings — Per anti-pattern instance, fix
-
Over-Engineered Findings — Heavy primitives for light workloads
-
Positive Findings — Concurrency choices that match workload
For each finding: code location, severity, confidence, the specific primitive change, and the impact (throughput, resource usage, correctness).
Want this applied to a live stack?
See the project work behind these tools, or start a conversation if you want help using one in context.