npm · solid-objects
Durable Objects on the SQL database you already run
Prevent race conditions in your Node app. Give each cart, booking, or job one actor that updates its state one call at a time, even across multiple processes. Keep its state and recovery logic together in your existing SQL database.
Read the five-minute guideWhy this library exists
Node’s event loop does not prevent async read-modify-write races. An in-process mutex can help, but coordinating multiple processes adds more pieces. This distributed-lock walkthrough separates Redis lock operations, renewal and retry logic, worker execution, and shutdown handling.
Your business logic shouldn’t need its own lock-management application. Solid Objects manages coordination so one actor can describe a cart, room, or job: its state, ordered operations, reminders, and recovery decisions. External actions still need idempotency; keeping the workflow together makes those decisions easier to follow.
How this compares
Four ways to keep two requests from corrupting one record. This table does not rank the projects. It shows what each one asks you to operate, and where each one stops.
| Solid Objects A library in your app | Cloudflare Durable Objects A managed platform | celld A daemon on your VMs | SQL transaction Already in your database | |
|---|---|---|---|---|
| Runs with no new service | Yes A Redis wake-up is optional | No The Cloudflare platform | No A daemon on every node | Yes |
| State lives in the database you already run | Yes SQLite, PostgreSQL, or MySQL | No Managed storage for each object | No One SQLite file for each object, in your bucket | Yes |
| One call at a time for each identity | Yes | Yes | Yes | No Only inside one transaction |
| Work continues after a restart | Yes | Yes | Yes | No The transaction rolls back |
| Timers that survive a deploy | Yes Reminders are rows | Yes Alarms | Yes Alarms | No A column and a sweeper |
| Side effects commit with the state | Yes A staged outbox | No You write it | No You write it | No |
| Realtime updates to open pages | Yes Committed projections | Yes WebSockets | Yes WebSockets | No |
| One transaction across two identities | No | No | No | Yes In the same database |
| Runs with no vendor account | Yes | No | Yes | Yes |
Delivery is ordered and at least once, so an external effect must be idempotent. Exactly once remains absent, despite its excellent branding. The full table, with a primary source for every row, is in docs/comparisons.md. External systems change. Check the primary source before you make an architecture decision.
Practical answers
Race conditions in Node.js: frequently asked questions
Lost updates, duplicate work, and recovery: what to use, what to keep, and where Solid Objects fits.
How can Node.js have race conditions if JavaScript is single-threaded?
An await lets another request run between reading a value and writing
it back. Two async operations can therefore read the same balance or stock count and
overwrite each other’s work. Multiple Node processes add another source of
concurrent writers. Solid Objects orders operations for each actor identity and
commits its durable state in SQL, so cooperating requests update one cart, room, or
job through the same owner.
How do I coordinate multiple Node processes without Redis locks?
Route each resource’s mutations through a Solid Objects actor with a stable identity, using the same supported database. The runtime uses SQL-backed ownership, a durable mailbox, and commit fencing to coordinate processes. SQLite, PostgreSQL, and MySQL are supported; Redis and a message broker are not required. Fencing rejects a stale owner’s actor commit even if its JavaScript is still running. It cannot fence an unrelated HTTP request or a database write that bypasses the supported actor APIs.
When should I use an async mutex, a row lock, or Solid Objects?
An in-memory mutex is useful when every competing caller lives in one process. A database transaction or row lock is useful when the whole invariant fits inside a short transaction. Solid Objects fits when a resource’s workflow spans processes, requests, delayed work, and restarts. It combines ordered calls, durable state, reminders, and recovery decisions in one actor instead of leaving your application to connect those pieces.
How do I prevent overselling and double booking in a Node app?
Make one actor own the availability for each stock item, event, or bookable
resource. Check availability and reserve it in the same operation; later callers see
the committed reservation. Stage a durable reminder to expire an unpaid hold rather
than relying on an in-memory setTimeout. Separate cart actors cannot
independently enforce a shared inventory limit, and Solid Objects has no cross-actor
transaction. The Node quickstart shows a reservation and
its expiry on one actor.
Can Solid Objects replace BullMQ and distributed locks for a per-resource workflow?
For a workflow centered on one cart, room, or job identity, Solid Objects can supply the ordered mailbox, delayed reminders, effect retries, and durable state using SQL. You do not need to add a Redis-backed queue solely to assemble that coordination. It is not a drop-in replacement for BullMQ or every general-purpose job queue. Existing workers can call actor operations when they need serialized changes to a particular resource.
What happens if Node crashes between saving state and enqueueing work?
When work is staged through the actor API, Solid Objects commits the state change and its reminders, messages, and effect intents together. Committed work stays in SQL and can resume when the runtime returns; uncommitted work can be retried. This avoids making your application separately save state and then enqueue the next step. Handlers must still guard against repeated delivery, and work does not execute while every runtime process is stopped.
Does Solid Objects guarantee that an API call or payment runs only once?
No. Ordered actor state changes and exactly-once external actions are different
guarantees. Effects are delivered at least once, so use a stable idempotency key
recognized by the external system. For replacement attempts, preserve the identity
of the logical action rather than assuming a new effect ID represents a new payment.
An onRecovery callback coordinates what the actor does after an
abandoned effect is retired; it does not cancel the old JavaScript or remote
request. See the
correctness contract.
Do I need another daemon or a paid cloud service to run Solid Objects?
No separate coordination daemon, Redis server, message broker, or paid hosted actor
service is required. Solid Objects is an MIT-licensed npm package backed by your
existing SQL database. Background work needs
runtime.run(signal) running in an application process, which can be
part of your Node deployment. You still operate that code and the database. Without
a running runtime, committed work waits in SQL; there is no invisible cloud worker
executing it for you.
Does serializing calls slow down my entire Node application?
Calls are ordered per actor identity, not through one global application lock. Different carts or rooms can run concurrently; one busy identity is intentionally sequential. Choose identities around the resource whose invariant needs protection, keep actor turns short, and stage slow external I/O as effects. Effects are outside the actor’s serialized turn and need their own idempotency. Throughput depends on your workload and database, so test a realistic hot resource rather than assuming unlimited scale.
When is Solid Objects the right choice for preventing Node race conditions?
It is a strong fit when shared state belongs to an identifiable resource and its workflow needs ordered updates, delayed steps, retries, and restart recovery without another infrastructure service. Use a simple SQL constraint or transaction when that is sufficient. It is pre-1.0 and does not provide cross-actor transactions or exactly-once external execution. The runnable Node guide lets you test concurrent reservations and recovery on your own database.