{"id":31294247,"url":"https://github.com/amrsaber/simple-redis-mutex","last_synced_at":"2025-09-24T19:21:07.107Z","repository":{"id":43383864,"uuid":"293331915","full_name":"AmrSaber/simple-redis-mutex","owner":"AmrSaber","description":"Simple and powerful distributed locking using Redis","archived":false,"fork":false,"pushed_at":"2025-06-19T21:44:54.000Z","size":412,"stargazers_count":30,"open_issues_count":0,"forks_count":3,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-09-21T01:28:25.723Z","etag":null,"topics":["lock","mutex","redis"],"latest_commit_sha":null,"homepage":"https://npmjs.com/package/simple-redis-mutex","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AmrSaber.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-09-06T17:52:39.000Z","updated_at":"2025-06-19T21:44:57.000Z","dependencies_parsed_at":"2024-06-19T17:12:42.758Z","dependency_job_id":"a4856aef-0e50-4705-a487-ab5aaf831584","html_url":"https://github.com/AmrSaber/simple-redis-mutex","commit_stats":{"total_commits":47,"total_committers":2,"mean_commits":23.5,"dds":"0.021276595744680882","last_synced_commit":"46f9cca5ac47b3d4f68af9da7ef3e7e7d6f8ee91"},"previous_names":["coligo-tech/simple-redis-lock"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/AmrSaber/simple-redis-mutex","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AmrSaber%2Fsimple-redis-mutex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AmrSaber%2Fsimple-redis-mutex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AmrSaber%2Fsimple-redis-mutex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AmrSaber%2Fsimple-redis-mutex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AmrSaber","download_url":"https://codeload.github.com/AmrSaber/simple-redis-mutex/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AmrSaber%2Fsimple-redis-mutex/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":276803620,"owners_count":25707718,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-24T02:00:09.776Z","response_time":97,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["lock","mutex","redis"],"created_at":"2025-09-24T19:21:05.371Z","updated_at":"2025-09-24T19:21:07.092Z","avatar_url":"https://github.com/AmrSaber.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Simple Redis Mutex\n\n\u003cp\u003e\n  \u003c!-- NPM version badge --\u003e\n  \u003ca href=\"https://www.npmjs.com/package/simple-redis-mutex\"\u003e\n    \u003cimg src=\"https://img.shields.io/npm/v/simple-redis-mutex\" alt=\"version\"/\u003e\n  \u003c/a\u003e\n\n  \u003c!-- Github \"Test Main\" workflow status --\u003e\n  \u003ca href=\"https://github.com/AmrSaber/simple-redis-mutex/actions\"\u003e\n    \u003cimg src=\"https://github.com/AmrSaber/simple-redis-mutex/workflows/Release/badge.svg?branch=master\" alt=\"Release Status\"/\u003e\n  \u003c/a\u003e\n\n  \u003c!-- Github \"Test Dev\" workflow status --\u003e\n  \u003ca href=\"https://github.com/AmrSaber/simple-redis-mutex/actions\"\u003e\n    \u003cimg src=\"https://github.com/AmrSaber/simple-redis-mutex/workflows/Test%20Dev/badge.svg?branch=dev\" alt=\"Test Dev Status\"/\u003e\n  \u003c/a\u003e\n\n  \u003c!-- NPM weekly downloads --\u003e\n  \u003ca href=\"https://www.npmjs.com/package/simple-redis-mutex\"\u003e\n    \u003cimg src=\"https://img.shields.io/npm/dw/simple-redis-mutex\" alt=\"weekly downloads\"/\u003e\n  \u003c/a\u003e\n\n  \u003c!-- License --\u003e\n  \u003ca href=\"https://github.com/AmrSaber/simple-redis-mutex/blob/master/LICENSE\"\u003e\n    \u003cimg src=\"https://img.shields.io/npm/l/simple-redis-mutex\" alt=\"license\"/\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\nImplements distributed mutex lock using redis as described in [redis docs](https://redis.io/commands/set#patterns). The term **simple** is opposed to the more complex **Redlock**, that was also proposed by Redis in their [docs](https://redis.io/topics/distlock) for use in case of distributed redis instances.\n\nLocks have timeout (expire time) and fail after options. Also, Redis Pub/Sub is used so that released lock can be immediately acquired by another waiting process instead of depending on polling. Manual polling is still supported though in case lock expires.\n\n## Install\n\nInstall the package using `npm`.\n```bash\nnpm i simple-redis-mutex\n```\n\nOr with bun\n```bash\nbun add simple-redis-mutex\n```\n\n## Examples\n\n```js\nimport { lock, tryLock } from 'simple-redis-mutex';\nimport { createClient, RedisClientType } from 'redis';\n\n// Connect to redis\nconst redis = await createClient()\n  .on('error', (err) =\u003e console.log('Redis Client Error', err))\n  .connect();\n\n// Using blocking lock\nasync function someFunction() {\n  // Acquire the lock, by passing redis client and the resource name (all settings are optional)\n  const release = await lock(redis, 'resource-name');\n\n  // Do some operations that require mutex lock\n  await doSomeCriticalOperations({ fencingToken: release.fencingToken! });\n\n  // Release the lock\n  await release();\n}\n\n// Using tryLock\nasync function someOtherFunction() {\n  const [hasLock, release] = await tryLock(redis, 'resource-name');\n  if (!hasLock) return; // Lock is already acquired\n\n  // Do some operations that require mutex lock\n  await doSomeCriticalOperations({ fencingToken: release.fencingToken! });\n\n  // Release the lock\n  await release();\n}\n```\n\n## Usage\n\nThere are 2 methods to acquire a lock:\n- `lock`: which attempts to acquire the lock in a blocking way, if lock is already acquired, it blocks until lock is available.\n- `tryLock`: which attempts to acquire the lock, if lock is already acquired it returns immediately.\n\n## API\n\n### `lock`\nAs per the code:\n```typescript\n/**\n * Attempts to acquire lock, if lock is already acquired it will block until it can acquire the lock.\n * Returns lock release function.\n *\n * Lock timeout is used to expire the lock if it's not been released before `timeout`.\n * This is to prevent crashed processes holding the lock indefinitely.\n *\n * When a lock is released redis Pub/Sub is used to publish that the lock has been released\n * so that other processes waiting for the lock can attempt to acquire it.\n *\n * Manual polling is also implemented to attempt to acquire the lock in case the holder crashed and did not release the lock.\n * It is controlled by `pollingInterval`.\n *\n * Application logic should not depend on lock timeout and polling interval. They are meant to be a safe net when things fail.\n * Depending on them is inefficient and an anti-pattern, in such case application logic should be revised and refactored.\n *\n * If process fails to acquire the lock before `failAfter` milliseconds, it will throw an error and call `onFail` if provided.\n * If `failAfter` is not provided, process will block indefinitely waiting for the lock to be released.\n *\n * @param redis redis client\n * @param lockName lock name\n * @param options lock options\n * @param options.timeout lock timeout in milliseconds, default: 30 seconds\n * @param options.pollingInterval how long between manual polling for lock status milliseconds, default: 10 seconds\n * @param options.failAfter time to fail after if lock is still not acquired milliseconds\n * @param options.onFail called when failed to acquire lock before `failAfter`\n * @returns release function\n */\nfunction lock(\n  redis: RedisClient,\n  lockName: string,\n  { timeout = DEFAULT_TIMEOUT, pollingInterval = DEFAULT_POLLING_INTERVAL, failAfter, onFail }: LockOptions = {},\n): Promise\u003cReleaseFunc\u003e\n```\n\n### `tryLock`\nAs per the code:\n```typescript\n/**\n * Try to acquire the lock, if failed will return immediately.\n * Returns whether or not the lock was acquired, and a release function.\n *\n * If the lock was acquired, release function is idempotent,\n * calling it after the first time has no effect.\n *\n * If lock was not acquired, release function is a no-op.\n *\n * @param redis redis client\n * @param lockName lock name\n * @param options lock options\n * @param options.timeout lock timeout in milliseconds, default: 30 seconds\n * @returns whether or not the lock was acquired and release function.\n */\nfunction tryLock(\n  redis: RedisClient,\n  lockName: string,\n  { timeout = DEFAULT_TIMEOUT }: TryLockOptions = {},\n): Promise\u003c[boolean, ReleaseFunc]\u003e \n```\n\n### `ReleaseFunc`\n```typescript\nexport type ReleaseFunc = (() =\u003e Promise\u003cvoid\u003e) \u0026 { fencingToken?: number };\n```\n\n## Notes\n\n### Redis Client\nThis package has **Peer Dependency** on [redis](https://www.npmjs.com/package/redis), the is the redis client that must be passed to lock functions.\n\nSame client must always be provided within same process, this is because pub/sub depends on the provided client and its lifecycle.\n\n### Lock options\nThe same lock can be acquired with different options each time, and it can be acquired using `lock` and `tryLock` in different places or under different circumstances (actually `lock` internally uses `tryLock` to acquire the lock). You can mix and match as you see fit, but I recommend always using the same options in same places for more consistency and to make debugging easier.\n\n`timeout` and `pollingInterval` have default value and user is not allowed to provide nullish values for those 2. This is for encouraging best practices. If you really want your lock to lock indefinitely for whatever reason, you can force-pass `null` for `timeout` and disable `pollingInterval` by also passing `null` (note that passing `undefined` will use the default values). Typescript will complain but you can just disable it for that line, something like so...\n```typescript\n// @ts-ignore\nawait lock(redis, 'some-lock', { timeout: null, pollingInterval: null });\n```\nBut I really advice against it. If lock-holding process crashes, there is no way to recover that lock other than removing the redis key manually from redis.\n\n### Lock Release\nOnce a lock is released a pub/sub channel is used to notify any process waiting for the lock. This makes waiting for lock more efficient and removes the need for frequent polling to check the status of the lock.\n\nA dedicated subscriber is created and managed in the background to manage subscribing to the pub/sub channel. It is created as a duplicate of provided redis client, and it stops whenever the provided client stops.\n\nOnly one subscriber is created at a time. If the client stops and reconnects for whatever reason, then subscriber will stop with it and will reconnect on next lock use.\n\n### Refresh lock timeout\nAt any point when using the lock if you need more time before the lock expires you can call `await release.refreshTimeout()` to reset the lock's timeout. e.g. if lock timeout was 5 seconds, and after 4 seconds you realize that you need more time to finish the task and you call `refreshTimeout` then lock timeout is reset to 5 seconds again.\n\nIf a process holds a lock and it is released or expired then that process calling `refreshTimeout` has no effect. Same thing if lock was not acquired in the first place (with `tryLock`) then `refreshTimeout` will have no effect.\n\n### Fencing Token\nA fencing token is an increasing number that is used to identify the order at which locks are acquired, and is used for further safety with writes in distributed systems. See \"Making the lock safe with fencing\" section from [this article](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html) for more info about fencing tokens.\n\nIf the lock is successfully acquired then a fencing token is issued, otherwise no fencing token will be issued or assigned if the lock is not acquired.\n\nFencing tokens can be access from `release` function like `release.fencingToken`, it is -1 only if lock was not acquired.\n\nFencing tokens are global across all locks issued and not scoped with lock name. Application logic should only depend on the fencing token increasing and not care about the exact value of the token.\n\n\n### Double Releasing\nOnce `release` function has been called all following calls are no-op, so same function cannot release the lock again from a different holder.\n\nIt's also taken into consideration that an expired lock cannot be released so it does not release the lock from another holder. i.e. if process A acquires the lock, then it expires, then process B acquires the lock. When process A tries to release the lock, it will not be released, as it's now acquired by B.\n\n### No Order Guarantees\nThis package does not guarantee that waiting tasks will execute in the same order they are waiting, the task that acquires the lock after it's been released is selected at random. See [this issue](https://github.com/AmrSaber/simple-redis-mutex/issues/19) for an example. If you need to guarantee the order, you should use a blocking queue and not a mutex.\n\n### Migration from v1.x\nBreaking Changes in v2:\n- Redis client is now `redis` and not `ioredis`\n- options have been renamed:\n  - `timeoutMillis` -\u003e `timeout`\n  - `retryTimeMillis` -\u003e `pollingInterval` -- and it is now only used for expired locks, other wise pub/sub is used with released locks\n  - `failAfterMillis` -\u003e `failAfter`\n- FIFO option has been removed: existing implementation was wrong, it failed on lock-holder crash or failing to acquire the lock, and I could not come up with an implementation that would retain the functionality using redis only -- I sincerely apologize to anyone who have used it.\n- `timeout` and `pollingInterval` have defaults. Locks are not allowed to lock indefinitely (except with work around mentioned in \"Lock Options\" section above).\n\n## Contribution\nYou are welcome to [open a ticket](https://github.com/AmrSaber/simple-redis-mutex/issues) anytime, if you find a bug or have a feature request.\n\nAlso feel free to create a PR to **dev** branch for bug fixes or feature suggestions.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Famrsaber%2Fsimple-redis-mutex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Famrsaber%2Fsimple-redis-mutex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Famrsaber%2Fsimple-redis-mutex/lists"}