Many applications need to maintain shared state across multiple requests. A shopping cart must preserve its contents as users navigate between pages. A multiplayer game needs to coordinate player actions. A rate limiter must track requests even when they arrive from different server instances.

These requirements become difficult when application code runs across multiple distributed workers. Each worker may handle a different request, and ordinary in-memory variables are not automatically shared between instances.

One approach is to store shared state in a database or coordinate access through an external service. However, this can introduce additional network calls and make synchronization more complicated.

Cloudflare Durable Objects provide another option. They allow developers to create stateful objects with a stable identity, a single logical point of coordination, and access to persistent storage. Requests associated with the same object are coordinated through that object's execution model, making it easier to manage shared state without building a separate coordination service for every use case.

This article explains how Durable Objects work, how to implement a basic stateful service, and what to consider when using them in production.

Why Shared State Is Difficult in Distributed Applications

Consider an application that maintains a counter for each customer.

A basic implementation might use an in-memory variable:

JavaScript
let counter = 0;

export default {
  async fetch() {
    counter++;

    return new Response(
      JSON.stringify({ counter }),
      {
        headers: {
          "Content-Type": "application/json"
        }
      }
    );
  }
};

This example works as a simple demonstration, but it does not provide reliable shared state in a distributed environment.

Different worker instances can have different values of counter. An instance may also be restarted, losing its in-memory value. Concurrent requests handled by different instances do not automatically coordinate their updates.

Moving the counter into a database can solve persistence problems, but applications may still need to manage concurrent updates and additional network requests.

A stateful coordination mechanism can simplify these requirements.

What Are Cloudflare Durable Objects?

Cloudflare Durable Objects are named, addressable objects that combine application logic with state and coordination.

Each Durable Object has a unique identity within its namespace. Requests routed to the same object are handled through that object's execution model, allowing it to coordinate operations on shared state.

A Durable Object can use persistent storage to preserve data beyond the lifetime of an individual request or in-memory object instance.

For example, an application might create separate Durable Objects for:

  • Individual shopping carts.
  • Multiplayer game rooms.
  • Collaborative document sessions.
  • Per-customer rate limits.
  • Real-time notification channels.
  • Shared counters and coordination tasks.

A shopping application could assign one object to each cart. Requests that update the same cart are routed to the same logical object, while requests for different carts can be handled independently.

This model is useful when state needs a clear owner and updates need to be coordinated.

How Durable Objects Work

A typical Durable Objects implementation has three parts.

1. The Worker Receives a Request

A Cloudflare Worker receives the incoming HTTP request. It determines which Durable Object should handle the operation.

For example, a request containing a cart ID can be routed to the object associated with that cart.

2. The Worker Obtains the Object Stub

The Worker uses a Durable Object namespace to identify an object and obtain a stub.

The stub acts as a communication interface to the selected object. The Worker can send requests through it without directly managing the object's runtime lifecycle.

3. The Durable Object Processes the Request

The Durable Object handles the request and reads or updates its state.

Its execution model provides coordination for requests handled by the same object. Persistent storage can be used to preserve state across restarts.

The following diagram summarizes the flow:

Code
Client Request
      |
      v
Cloudflare Worker
      |
      v
Select Durable Object ID
      |
      v
Durable Object Stub
      |
      v
Durable Object Logic
      |
      v
Persistent Storage
      |
      v
Response to Client

The important design decision is choosing the object identity. Requests that must coordinate should use the same object ID, while independent state should be assigned to different objects.

Building a Persistent Counter

A counter is a useful way to understand the basic programming model.

The following example uses the current-style JavaScript Durable Objects API with persistent storage through ctx.storage. It illustrates the core implementation, while deployment configuration and compatibility should be checked against the Cloudflare Workers runtime used by the project.

Step 1: Implement the Durable Object

Create a class named Counter:

JavaScript
export class Counter {
  constructor(ctx, env) {
    this.ctx = ctx;
    this.env = env;
  }

  async fetch(request) {
    const current = await this.ctx.storage.get("count") ?? 0;

    const next = current + 1;

    await this.ctx.storage.put("count", next);

    return Response.json({
      count: next
    });
  }
}

The object reads the current count from persistent storage, increments it, and saves the updated value.

Unlike a plain global variable, the stored value can survive an object restart.

This example assumes that every request increments the counter. A production API would usually separate read and update operations, validate the HTTP method, and handle errors explicitly.

Step 2: Route Requests to the Object

The Worker can select a named Durable Object and send it a request:

JavaScript
export default {
  async fetch(request, env) {
    const id = env.COUNTERS.idFromName("global-counter");

    const stub = env.COUNTERS.get(id);

    return stub.fetch(request);
  }
};

Here, COUNTERS is a Durable Object namespace binding configured in the Worker environment.

Using the same name, global-counter, routes requests to the same logical object within that namespace.

For a customer-specific counter, the name could instead be derived from a validated customer identifier.

Do not use arbitrary client-provided names without validation and authorization. Otherwise, users may be able to create or access object identities that the application did not intend to expose.

Step 3: Configure the Binding

The Worker must declare its Durable Object binding and class in the deployment configuration. The exact configuration syntax depends on the project's Cloudflare tooling and migration setup.

A configuration typically identifies the binding name, the exported Durable Object class, and the applicable migration entry.

After configuring the binding, deploy the Worker and test requests against the deployed environment.

A local development test is useful, but it should not replace testing the actual deployment configuration and runtime behavior.

Maintaining Consistency Across Concurrent Requests

Durable Objects are particularly useful when several requests update the same logical state.

Suppose two requests arrive to increment a counter. If each request reads the same old value and writes an independently calculated result, an ordinary read-modify-write sequence could lose an update in a system without proper coordination.

Durable Objects provide a single logical execution context for the selected object, but developers still need to design their operations correctly.

In the example above, each request performs a storage read followed by a storage write. For a simple demonstration, this is understandable, but a production implementation should use an atomic storage operation or a transaction when the update requires stronger guarantees.

For example, where supported by the storage API, a transaction can group the read and write:

JavaScript
async function incrementCounter(storage) {
  return storage.transaction(async txn => {
    const current = await txn.get("count") ?? 0;
    const next = current + 1;

    await txn.put("count", next);

    return next;
  });
}

This example illustrates the transaction pattern. Confirm the exact transaction API and supported operations for the storage interface and runtime version used by the application.

The principle is important: execution coordination and transaction guarantees solve related but distinct problems. Use the appropriate storage operation for the consistency requirement.

Choosing a Durable Object Identity

Object identity determines which requests share state.

For example, consider a collaborative application with multiple document rooms.

A room-based identity might look like this:

JavaScript
const id = env.ROOMS.idFromName(roomId);
const stub = env.ROOMS.get(id);

Every request using the same validated roomId reaches the same logical object.

This makes the object suitable for managing room membership, coordinating edits, and broadcasting updates.

However, using one global object for every user or room can create a bottleneck. A busy object has limited processing capacity and can become a hot spot.

A good design distributes independent workloads across multiple object identities while keeping related state together.

Use case

Possible object identity

Reason

Shopping cart

Cart ID

Each cart maintains independent state

Multiplayer game

Game room ID

Players in one room need coordination

Customer rate limit

Customer or account ID

Requests share a rate-limit state

Collaborative document

Document ID

Edits need coordination for one document

Global counter

Fixed name

Every request intentionally updates one counter

The correct granularity depends on the consistency requirements and expected request volume.

Durable Objects and WebSockets

Durable Objects are also useful for real-time applications that need persistent connections.

For example, a chat room may need to track connected users and distribute messages to clients in the same room.

A Durable Object can manage the room's WebSocket connections and coordinate broadcasts. The Worker can route connection requests to the object associated with the requested room.

This avoids requiring every Worker instance to maintain a separate, inconsistent list of room participants.

For production systems, the implementation must also handle disconnected clients, reconnections, authentication, room membership, and message delivery behavior.

Persistent state and live connections should be treated separately. Saving a message to storage does not automatically guarantee that every connected client received it.

Common Mistakes

Using In-Memory Variables for Persistent Data

In-memory fields can be useful for caching or temporary runtime information, but they should not be the only copy of state that must survive restarts.

Use Durable Object storage for persistent application data.

Creating One Object for Everything

A single object can become a bottleneck if it receives all application traffic.

Choose object identities that keep related state together while allowing unrelated workloads to run independently.

Assuming Every Operation Is Automatically Atomic

The Durable Object execution model provides coordination, but multi-step operations still need suitable storage semantics.

Use transactions or atomic storage operations when an update must not be partially applied or overwritten by another operation.

Trusting Object IDs Supplied by Clients

Object identity is not an authorization mechanism.

A client that knows an object name should not automatically be allowed to read or update its state. Validate access permissions before forwarding requests.

Ignoring Storage and Runtime Limits

Durable Objects have platform-specific limits and operational characteristics. Large objects, excessive storage operations, and high request volumes can affect performance.

Review current platform limits and monitor the actual workload before scaling the design.

Troubleshooting Durable Objects

Problem

Possible cause

What to check

The object cannot be found

Incorrect binding or deployment configuration

Verify the namespace binding and class registration

State disappears after restart

Data was stored only in memory

Persist required values using object storage

Updates are missing

Incorrect operation design or failed writes

Review transaction boundaries and storage errors

Requests become slow

A single object is receiving too much traffic

Reconsider object identity and workload distribution

A user accesses another user's data

Missing authorization checks

Validate identity and permissions before routing

WebSocket messages are inconsistent

Connection lifecycle or broadcast logic is incomplete

Test reconnects, disconnections, and room membership

Advantages and Limitations

Durable Objects simplify stateful coordination by providing stable object identities, persistent storage, and a clear place to manage shared state.

They are particularly useful when multiple requests need to coordinate around one logical entity. They can also reduce the need to build a separate coordination service for relatively focused workloads.

However, Durable Objects are not a replacement for every database or distributed processing system. A single object can become a hot spot, and applications must still consider storage semantics, authorization, failure handling, and platform limits.

Workloads that require large analytical queries, complex relational transactions across many entities, or extensive data processing may be better served by a database or another specialized service.

Best Practices for Production Applications

  • Choose object identities based on the state that must be coordinated.
  • Persist data that must survive restarts.
  • Use transactions or atomic operations for multi-step updates.
  • Validate and authorize requests before accessing an object.
  • Avoid routing unrelated high-volume workloads through one object.
  • Monitor request latency, storage operations, errors, and hot objects.
  • Test recovery, concurrent requests, and WebSocket reconnections.
  • Review Cloudflare's current runtime and storage limits before deployment.
  • Keep business logic separate from transport and routing logic where practical.

Summary

Cloudflare Durable Objects provide a practical model for maintaining shared state across distributed requests. By routing related operations to the same logical object, applications can coordinate updates without relying on ordinary per-instance memory.

The approach works well for shopping carts, game rooms, collaborative sessions, counters, and other stateful workloads. The key is to select suitable object identities, persist important data, and use appropriate transaction semantics.

Before deploying to production, test concurrent updates, authorization, recovery, and expected request volume. Durable Objects can simplify state management, but a sound data model and careful operational design remain essential.