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 guide

Why 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.

Try the five-minute guide