Build a counter
Build a counter using Durable Objects and Workers with RPC methods.
This example shows how to build a counter using Durable Objects and Workers with RPC methods that can print, increment, and decrement a name provided by the URL query string parameter, for example, ?name=A.
import { DurableObject } from "cloudflare:workers";
// Workerexport default {  async fetch(request, env) {    let url = new URL(request.url);    let name = url.searchParams.get("name");    if (!name) {      return new Response(        "Select a Durable Object to contact by using" +          " the `name` URL query string parameter, for example, ?name=A",      );    }
    // Every unique ID refers to an individual instance of the Counter class that    // has its own state. `idFromName()` always returns the same ID when given the    // same string as input (and called on the same class), but never the same    // ID for two different strings (or for different classes).    let id = env.COUNTERS.idFromName(name);
    // Construct the stub for the Durable Object using the ID.    // A stub is a client Object used to send messages to the Durable Object.    let stub = env.COUNTERS.get(id);
    // Send a request to the Durable Object using RPC methods, then await its response.    let count = null;    switch (url.pathname) {      case "/increment":        count = await stub.increment();        break;      case "/decrement":        count = await stub.decrement();        break;      case "/":        // Serves the current value.        count = await stub.getCounterValue();        break;      default:        return new Response("Not found", { status: 404 });    }
    return new Response(`Durable Object '${name}' count: ${count}`);  },};
// Durable Objectexport class Counter extends DurableObject {  async getCounterValue() {    let value = (await this.ctx.storage.get("value")) || 0;    return value;  }
  async increment(amount = 1) {    let value = (await this.ctx.storage.get("value")) || 0;    value += amount;    // You do not have to worry about a concurrent request having modified the value in storage.    // "input gates" will automatically protect against unwanted concurrency.    // Read-modify-write is safe.    await this.ctx.storage.put("value", value);    return value;  }
  async decrement(amount = 1) {    let value = (await this.ctx.storage.get("value")) || 0;    value -= amount;    await this.ctx.storage.put("value", value);    return value;  }}import { DurableObject } from "cloudflare:workers";
export interface Env {  COUNTERS: DurableObjectNamespace<Counter>;}
// Workerexport default {  async fetch(request, env) {    let url = new URL(request.url);    let name = url.searchParams.get("name");    if (!name) {      return new Response(        "Select a Durable Object to contact by using" +          " the `name` URL query string parameter, for example, ?name=A",      );    }
    // Every unique ID refers to an individual instance of the Counter class that    // has its own state. `idFromName()` always returns the same ID when given the    // same string as input (and called on the same class), but never the same    // ID for two different strings (or for different classes).    let id = env.COUNTERS.idFromName(name);
    // Construct the stub for the Durable Object using the ID.    // A stub is a client Object used to send messages to the Durable Object.    let stub = env.COUNTERS.get(id);
    let count = null;    switch (url.pathname) {      case "/increment":        count = await stub.increment();        break;      case "/decrement":        count = await stub.decrement();        break;      case "/":        // Serves the current value.        count = await stub.getCounterValue();        break;      default:        return new Response("Not found", { status: 404 });    }
    return new Response(`Durable Object '${name}' count: ${count}`);  },} satisfies ExportedHandler<Env>;
// Durable Objectexport class Counter extends DurableObject {  async getCounterValue() {    let value = (await this.ctx.storage.get("value")) || 0;    return value;  }
  async increment(amount = 1) {    let value: number = (await this.ctx.storage.get("value")) || 0;    value += amount;    // You do not have to worry about a concurrent request having modified the value in storage.    // "input gates" will automatically protect against unwanted concurrency.    // Read-modify-write is safe.    await this.ctx.storage.put("value", value);    return value;  }
  async decrement(amount = 1) {    let value: number = (await this.ctx.storage.get("value")) || 0;    value -= amount;    await this.ctx.storage.put("value", value);    return value;  }}Finally, configure your Wrangler file to include a Durable Object binding and migration based on the namespace and class name chosen previously.
{  "name": "my-counter",  "main": "src/index.ts",  "durable_objects": {    "bindings": [      {        "name": "COUNTERS",        "class_name": "Counter"      }    ]  },  "migrations": [    {      "tag": "v1",      "new_sqlite_classes": [        "Counter"      ]    }  ]}name = "my-counter"main = "src/index.ts"
[[durable_objects.bindings]]name = "COUNTERS"class_name = "Counter"
[[migrations]]tag = "v1"new_sqlite_classes = ["Counter"]Was this helpful?
- Resources
- API
- New to Cloudflare?
- Products
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- 2025 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark