
Research
TeamPCP Compromises Telnyx Python SDK to Deliver Credential-Stealing Malware
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.
async-mutex-lite
Advanced tools
🔒 Tiny keyed async mutex for JavaScript & TypeScript — ~400–600 bytes gzip, zero dependencies.
Have you ever run into a situation where two async processes run at the same time and produce inconsistent results? That’s exactly the problem this library solves.
async-mutex-lite ensures that tasks with the same key run sequentially, while tasks with different keys can still run in parallel without interfering with each other.
npm install async-mutex-lite
JavaScript is single-threaded, but race conditions can still happen when using async/await.
Imagine this scenario:
// ❌ Dangerous — two requests arrive at the same time for the same user
app.post("/checkout", async (req) => {
const balance = await getBalance(req.userId) // both read: $100
if (balance >= req.amount) {
await deductBalance(req.userId, req.amount) // both deduct
await createOrder(req.userId)
}
})
If two requests arrive almost simultaneously, they may both read the same balance before either has deducted it. The result: the balance gets deducted twice, but maybe only one order is created — or worse.
With async-mutex-lite:
// ✅ Safe — requests for the same user are processed sequentially
app.post("/checkout", async (req) => {
await mutex(`checkout:${req.userId}`, async () => {
const balance = await getBalance(req.userId)
if (balance >= req.amount) {
await deductBalance(req.userId, req.amount)
await createOrder(req.userId)
}
})
})
Requests from other users (different userId) can still run in parallel — only requests for the same user are queued.
This library uses Promise chaining instead of a traditional queue.
mutex("user:1", taskA) ─┐
mutex("user:1", taskB) ─┼─► taskA → taskB → taskC (sequential, FIFO)
mutex("user:1", taskC) ─┘
mutex("user:2", taskD) ────► taskD (parallel)
Each key has its own promise chain.
New tasks always wait for the previous task in the chain to finish.
After all tasks complete, the internal memory is cleaned automatically — no memory leaks.
# npm
npm install async-mutex-lite
# pnpm
pnpm add async-mutex-lite
# yarn
yarn add async-mutex-lite
# bun
bun add async-mutex-lite
Compatible with:
import { mutex } from "async-mutex-lite"
// Async function
const result = await mutex("my-key", async () => {
const data = await fetchSomething()
return data
})
// Sync functions are also supported
const value = await mutex("my-key", () => {
return 42
})
Use async-mutex-lite when you have async operations that must not run concurrently for the same resource.
Prevent double-charges or negative balances.
await mutex(`wallet:${userId}`, () => processPayment(userId, amount))
await mutex(`webhook:${webhookId}`, () => processWebhook(webhookId))
await mutex("log-file", () => fs.appendFile("app.log", logLine))
async function getCachedUser(userId: string) {
if (cache.has(userId)) return cache.get(userId)
return mutex(`cache:${userId}`, async () => {
if (cache.has(userId)) return cache.get(userId)
const user = await db.findUser(userId)
cache.set(userId, user)
return user
})
}
await mutex(`product:${productId}`, async () => {
const stock = await getStock(productId)
if (stock > 0) await decrementStock(productId)
})
await mutex(`api-call:${userId}`, () => callExternalAPI(userId))
Mutex only helps with concurrency on shared async state.
mutex(key, task, options?)function mutex<T>(
key: string,
task: () => Promise<T> | T,
options?: MutexOptions
): Promise<T | undefined>
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | ✅ | Resource identifier. Tasks with the same key are queued. |
task | () => Promise<T> | T | ✅ | Function to execute. Can be async or sync. |
options | MutexOptions | ❌ | Optional configuration. |
Returns the value returned by task.
If a task is skipped due to "stop" strategy, the return value will be undefined.
interface MutexOptions {
onError?: "continue" | "stop"
}
Default:
continue
"continue" (default)The queue continues even if a task fails.
const t1 = mutex("key", () => { throw new Error("failed") }).catch(console.error)
const t2 = mutex("key", () => "this task still runs ✅")
await Promise.all([t1, t2])
Use this when failures should not block other tasks.
"stop"If a task fails with "stop" strategy, all pending tasks in the same queue are cancelled.
const t1 = mutex("key", () => { throw new Error("failed") }, { onError: "stop" })
.catch(console.error)
const t2 = mutex("key", () => "this task will NOT run ❌")
await Promise.allSettled([t1, t2])
Use this for all-or-nothing operations, like transactional workflows.
After the queue drains, the key resets automatically.
import express from "express"
import { mutex } from "async-mutex-lite"
const app = express()
app.post("/checkout", async (req, res) => {
const { userId, productId, quantity } = req.body
try {
await mutex(`checkout:${userId}`, async () => {
const [balance, stock] = await Promise.all([
getBalance(userId),
getStock(productId),
])
if (balance < req.body.total) throw new Error("Insufficient balance")
if (stock < quantity) throw new Error("Insufficient stock")
await Promise.all([
deductBalance(userId, req.body.total),
deductStock(productId, quantity),
createOrder({ userId, productId, quantity }),
])
})
res.json({ success: true })
} catch (err) {
res.status(400).json({ error: err.message })
}
})
import { mutex } from "async-mutex-lite"
export async function POST(req: Request) {
const { email } = await req.json()
await mutex(`subscribe:${email}`, async () => {
const exists = await db.user.findUnique({ where: { email } })
if (exists) throw new Error("Email already registered")
await db.user.create({ data: { email } })
await sendWelcomeEmail(email)
})
return Response.json({ message: "Subscription successful!" })
}
import { mutex } from "async-mutex-lite"
async function handleWebhook(event: WebhookEvent) {
await mutex(`webhook:${event.id}`, async () => {
const alreadyProcessed = await db.webhook.findUnique({
where: { id: event.id }
})
if (alreadyProcessed) return
await processEvent(event)
await db.webhook.create({ data: { id: event.id } })
})
}
const user = await mutex("fetch-user", async () => {
return await db.user.findFirst()
})
// user: User | null | undefined
| Library | Size | Keyed Lock | Error Strategy | TypeScript |
|---|---|---|---|---|
| async-lock | ~5 KB | ✅ | ❌ | Partial |
| async-mutex | ~3 KB | ❌ | ❌ | ✅ |
| await-lock | ~1 KB | ❌ | ❌ | ❌ |
| async-mutex-lite | ~0.5 KB | ✅ | ✅ | ✅ |
Yes. The library has no dependencies, a very small surface area, and 100% test coverage.
Yes, but remember: each serverless instance has its own memory.
Mutex works only when conflicting requests are handled by the same instance.
For cross-instance coordination you still need an external lock (e.g., Redis).
Yes. Tasks are executed exactly in the order they were scheduled.
task returns undefined?Then mutex returns undefined.
This is indistinguishable from a skipped task when using "stop" strategy.
Yes. The package provides both ESM (.js) and CommonJS (.cjs) builds.
git clone https://github.com/deni-irawan-nugraha/async-mutex-lite.git
cd async-mutex-lite
npm install
npm test
npm run test:coverage
npm run test:bench
npm run build
MIT License — free to use, modify, and distribute.
Made with ❤️ by Deni Irawan Nugraha
GitHub: https://github.com/deni-irawan-nugraha
npm: https://www.npmjs.com/package/async-mutex-lite
FAQs
A tiny keyed async mutex for JavaScript / TypeScript
We found that async-mutex-lite demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.

Security News
TeamPCP is partnering with ransomware group Vect to turn open source supply chain attacks on tools like Trivy and LiteLLM into large-scale ransomware operations.

Security News
/Research
Widespread GitHub phishing campaign uses fake Visual Studio Code security alerts in Discussions to trick developers into visiting malicious website.