Run Cloudflare Workers Locally

Run a stateful Cloudflare Workers app locally with celld.

Introduction

30 Second Summary

A stateful app can forget everything the moment its server stops. Every restart risks sending its data back to zero.

In this project, you will run a Cloudflare Workers counter app on your laptop with celld, the open source runtime from the Deno team. You will prove its Durable Object keeps state across restarts without a Cloudflare account.

What You'll Build

You refresh a local page to watch its count climb before restarting the app to see the next request continue from the same number.

By the end of this project, you'll have:

  • A repo you own with a local Workers app that responds in your browser.
  • A live counter that increases whenever your browser sends an HTTP request.
  • A persistence test that restarts celld and shows the count continuing from its stored value in .celld/dev.
  • Secret Mission: An optional challenge to push your local Workers skills further.

Are there any prerequisites?

You need a terminal plus a code editor. Prebuilt celld binaries support Apple silicon macOS plus Linux on x86-64 or ARM64.

Windows requires WSL because native Windows is unsupported. Intel Macs are also unsupported.

Before We Start

Before the hands-on work begins, this is your chance to lock in the goal. You are preparing to prove that a local app can keep its state across a restart.

Install celld

A Cloudflare Workers app needs a runtime that understands its APIs. The local celld runtime gives your laptop that foundation without a Cloudflare account.

This step installs celld from its official README. A version check then proves your terminal can find the CLI.

In this step, get ready to:
  • Confirm that your laptop supports a prebuilt celld binary.
  • Install celld with the official README script.
  • Verify that the celld CLI prints its version.
Check platform support

Prebuilt celld binaries only cover certain processors. Windows learners use WSL because native Windows is unsupported.

✔️ I use Apple silicon macOS or Linux

The prebuilt installer supports Apple silicon macOS. It also supports Linux on x86-64 or ARM64.

  • Keep using the terminal from earlier for the installation.

ⓧ I use an Intel Mac

The prebuilt celld binaries do not support Intel Macs. This installation path requires a supported machine.

  • Switch to an Apple silicon Mac or a supported Linux machine before continuing.

ⓧ I use Windows

Native Windows is unsupported. WSL provides the supported Linux environment for this project.

  • Use WSL on your Windows laptop before continuing.
  • Return to this step in a WSL terminal.
Install celld from the README

The official installer downloads a prebuilt celld binary to your laptop. It keeps the installation inside your local user directories.

  • Install celld with the README script by running this command:
curl -fsSL https://celld.dev/install.sh | sh

What does this installer do?

  • The curl command downloads the official installer over HTTPS.
  • The pipe passes the installer to sh for execution.
  • The installer stores celld releases under ~/.local/lib/celld/releases.
  • The installer may ask you to add ~/.local/bin to your PATH.
  • Follow the installer prompt to add ~/.local/bin to your PATH if requested.
  • Confirm that the installer finishes without reporting an error.

Installer did not finish?

Confirm that your internet connection can reach the celld website. Retry the installer after the connection is stable.

If the binary installs but your terminal cannot find it, follow the PATH instruction printed by the installer.

Help me diagnose why the celld installer did not complete on my supported laptop.

Verify the celld CLI

A successful installation only helps when your shell can find the executable through PATH. The version command tests that connection.

Before you run the check, predict whether your current terminal can find celld on its PATH.

  • Verify the celld installation by running this command:
celld --version

What should you see?

Your terminal prints the celld version number. That output proves the local CLI is installed successfully.

✔️ Awesome, celld is ready

That is the setup complete: your terminal can invoke celld locally. The celld --version command successfully prints its version.

ⓧ I'd like to double check the final state

The finished state has two checks. Compare your terminal with each one.

  • Confirm that celld was installed locally with the official README script.
  • Confirm that celld --version prints its version in the terminal.
  • Follow the installer-provided PATH instruction if your terminal cannot find celld.
  • Retry the version command shown above.

Terminal cannot find celld?

The installer may have placed celld in ~/.local/bin without updating your current shell. Apply the PATH instruction that the installer printed.

Help me make the celld command available in my terminal.

Your local celld runtime is ready. Next, you will copy the hello example into your own folder and run its Worker on your laptop.

Run the Hello Example

The celld version check proved the runtime is available on your laptop. Now you need a real Worker project to prove it can serve a request.

A Cloudflare Worker needs source code plus a configuration file. The official hello example supplies both without requiring a Cloudflare account.

In this step, get ready to:
  • Copy the official hello example into a local project folder.
  • Start the hello Worker with the local development server.
  • Open the local address to see the Worker's response.
Copy the hello example

The official examples are small Wrangler projects that already contain the files celld expects. Git gives you a local copy of those examples.

  • Return to the terminal where you verified celld.
  • Copy the celld repository to your Desktop by running these commands:
cd ~/Desktop
git clone https://github.com/denoland/celld

What do these commands do?

  • The first command makes your Desktop the location for the local copy.
  • The second command downloads the official source into a folder named celld.
  • Enter the hello example folder by running this command:
cd celld/examples/hello

Where are you now?

The celld/examples/hello directory is now your current project folder. The development command runs against the files in this location.

  • Confirm the hello project files exist by running this command:
ls

What should you see?

The output includes index.js plus wrangler.jsonc. These files provide the Worker code plus its configuration.

Unable to copy the example?

  • Check your internet connection if the repository download does not finish.
  • Rename any existing celld folder on your Desktop in Finder or File Explorer.
  • Retry the download command after the original folder name is available.

Help me copy the official celld hello example into the correct local folder.

Start the local Worker

Development mode starts one local celld node with a local object store. The process serves the Worker directly from your laptop.

Before you start the server, do you expect celld to ask for Cloudflare credentials?

  • Start the hello Worker by running this command:
celld dev

What does this command start?

  • The command reads the Worker source plus its Wrangler configuration from the current folder.
  • Celld starts a local listener without a cloud bucket or Cloudflare account.
  • The terminal prints the local address for the running Worker.
  • The process keeps control of this terminal while the server is running.
  • Record the address printed in the terminal: http://127.0.0.1:9876.
  • Leave the development process running in this terminal.

Local address not printed?

  • Confirm your current folder contains index.js plus wrangler.jsonc.
  • Stop any other local process that celld reports is using port 9876.
  • Retry the development command from celld/examples/hello.

Help me diagnose why celld dev does not print a local address for the hello example.

Open the hello response

The printed address points your browser at the Worker listener on your laptop. Loading it sends a real HTTP request through celld.

Before you load the address, what kind of response do you expect from a project named hello?

  • Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
  • Type the name of your web browser into the search bar.
  • Press Enter to open the browser.
  • Paste http://127.0.0.1:9876 into the browser's address bar.
  • Press Enter to load the Worker.

You'll see Hello from cells! url= followed by the local address. That's your first Worker running entirely on your laptop.

Browser cannot reach the Worker?

  • Confirm the development process is still running in the terminal.
  • Copy http://127.0.0.1:9876 from the terminal again.
  • Paste the copied address into the browser without adding another path.

Help me connect my browser to the hello Worker running under celld.

✔️ Awesome, I've got everything!

Your hello Worker is live on your laptop. The local project folder plus the running server are ready for the stateful counter example.

ⓧ I'd like to double check the final state

  • Confirm celld/examples/hello exists inside the celld folder on your Desktop.
  • Confirm the hello project folder contains index.js plus wrangler.jsonc.
  • Confirm the terminal is running the development server from the hello project folder.
  • Confirm the terminal shows a local address for the Worker.
  • Confirm the browser shows Hello from cells! url= followed by that local address.
  • Confirm you completed the check without a Cloudflare account or deployment.

Your local runtime now serves a real Worker request. Next, you'll replace the hello response with a stateful counter.

Switch to the Counter Example

Your hello Worker is already running locally with celld. Now you will replace its stateless response with a counter backed by a Durable Object.

A stateless Worker cannot preserve a changing value by itself. The counter example stores its value before returning the latest count to your browser.

In this step, get ready to:
  • Replace the hello files with the official counter example.
  • Run the counter Worker with celld.
  • Confirm that repeated browser requests increase the count.
Prepare the local runtime

The running hello process currently owns the terminal session. Stopping it gives you a clean point to replace both project files.

  • Return to the terminal running the hello Worker.
  • Stop the hello development server by pressing Ctrl+C.
  • Replace the hello Worker files with the official counter files by running these commands:
curl --fail --location --output index.js https://raw.githubusercontent.com/denoland/celld/main/examples/counter/index.js
curl --fail --location --output wrangler.jsonc https://raw.githubusercontent.com/denoland/celld/main/examples/counter/wrangler.jsonc

What did the download change?

  • The first command replaces index.js with the counter Worker and its counter class.
  • The second command replaces wrangler.jsonc with the COUNTER binding and migration.
  • The download writes the official example directly into your existing local project folder.

Did the download fail?

Confirm that the terminal is still inside the local project folder from the previous step. Check your internet connection before running both commands again.

Help me diagnose the failed counter example download.

✔️ Awesome, I've got everything!

Your local project now contains the official counter Worker and its Durable Object configuration.

ⓧ I'd like to double check the full code

Your index.js file should match this official counter Worker:

export class Counter {
 constructor(state, env) {
 this.state = state;
 }
 async fetch(request) {
 let n = (await this.state.storage.get("n")) ?? 0;
 n++;
 await this.state.storage.put("n", n);
 return Response.json({ n, url: request.url });
 }
}
export default {
 async fetch(request, env) {
 const name = new URL(request.url).searchParams.get("name") ?? "default";
 const id = env.COUNTER.idFromName(name);
 return env.COUNTER.get(id).fetch(request);
 },
};

Your wrangler.jsonc file should match this configuration:

{
 "name": "counter",
 "main": "index.js",
 "compatibility_date": "2026-01-01",
 "durable_objects": {
 "bindings": [{ "name": "COUNTER", "class_name": "Counter" }]
 },
 "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Counter"] }]
}
Serve the new configuration

The updated configuration connects the Worker to its counter object. The local development command builds that configuration before serving requests.

This process keeps the terminal busy while the Worker is running. Leave this terminal session running for the browser check.

  • Start the counter Worker from the local project folder by running this command:
celld dev

What does this command start?

  • The command builds the updated Worker configuration.
  • celld starts one local node with a local object store.
  • The terminal prints the local address for the counter Worker.

Good, the counter Worker is now listening at the local address printed in your terminal.

Is the counter Worker unavailable?

Confirm that both downloaded files are in the same local project folder. A failed download can leave the earlier hello file in place.

Help me diagnose why the local counter Worker did not start.

Prove each request changes state

Every browser request reaches the same named counter object. That object reads n from storage before increasing it by one.

Before you refresh, consider whether the next value of n will stay fixed or increase.

  • Switch back to the browser tab showing the local address from earlier.
  • Refresh the page once.

You will see a JSON response containing n and url. The value of n confirms that the Durable Object handled the request.

  • Refresh the page three more times.

You will see n increase after every refresh. That rising value proves the same local Durable Object is updating its stored count.

Does the count stay fixed?

Confirm that the browser uses the local address printed by the current counter process. Check that wrangler.jsonc contains the COUNTER binding from the full-code check.

Help me find why my local counter does not increase.

You have turned the stateless hello project into a working local counter. Each browser request now updates state through its Durable Object.

Your counter is increasing locally. Next, you will restart celld to prove that the stored value survives the process stopping.

Verify Durable Object Persistence

Your counter now climbs with every browser refresh. Each request reaches the local Durable Object.

The remaining question is whether that count survives when celld stops. Restarting the local process separates persisted state from process memory.

In this step, get ready to:
  • Record a pre-restart counter value.
  • Restart the local celld process.
  • Prove that the counter continues from its recorded value.
Establish the baseline

A baseline gives you a specific value to compare after the restart. Any later value above this point shows that the new process loaded the existing counter state.

  • Switch back to the browser tab from earlier.
  • Refresh the local address once.
  • Record the displayed value here: your pre-restart count.

This value marks the counter state immediately before the first process stops.

Stop the first process

A restart tests whether the count can outlive the process that handled the earlier requests. The new process must load the same object state for the count to continue.

  • Switch back to the terminal panel running the counter Worker.
  • Press Ctrl+C to stop the first process.
  • Start a new process from the same local project folder by running this command:
celld dev

What does this command start?

  • The command starts a fresh local celld development process for the counter Worker.
  • The printed local address identifies where the restarted Worker is serving requests.
  • The command stays active while the local server is running.

No local address after restarting?

  • Confirm the first process ended after you pressed Ctrl+C.
  • Confirm the terminal remained in the local project folder containing the counter example.

Help me diagnose why the restarted celld development process is not serving the local counter Worker.

Test the restarted counter

Before you refresh, do you expect the counter to reset or continue beyond your recorded value?

  • Switch back to the browser tab from earlier.
  • Refresh the local address once.

You should see a value higher than your pre-restart count. The pre-restart state survived because the Durable Object stored its count locally on disk.

You have the proof: your Durable Object kept its count across a full celld restart.

Did the counter start over?

  • Confirm the restarted process is running from the same local project folder as the first process.
  • Confirm the browser uses the local address printed by the restarted process.
  • Refresh only after the restarted process reports that it is serving requests.

Help me find why my local Durable Object counter reset after restarting celld.

✔️ The counter continued after restart

The restarted Worker is serving requests from the same local project folder. Its counter continues beyond the value recorded before the restart.

ⓧ I'd like to double check the final state

  • Confirm the celld CLI remains installed after the successful version check from earlier.
  • Confirm the same local project folder contains the counter Worker with its Durable Object binding and counter logic.
  • Confirm a restarted celld dev process is running from that local project folder.
  • Confirm the browser uses the local address printed by the restarted process.
  • Confirm the persisted counter increases by 1 for each browser request.
  • Confirm the counter continues beyond its pre-restart value.
  • Confirm no Cloudflare account or deployment was used.

Your counter now survives a local runtime restart. Next, you will inspect the storage behind that persistence.

Inspect the Local Object Store

The restarted counter already proved that its Durable Object survives a process restart. You can now trace that persistence to the files celld keeps inside your local project folder.

The .celld/dev folder is the local development object store. Inspecting it connects the browser result to the state stored on your laptop.

In this step, get ready to:
  • Reveal the hidden .celld/dev folder.
  • Inspect the local object store without changing its contents.
  • Confirm the counter still increments by 1 per request.
Reveal the hidden object store

Finding .celld can be fiddly because file explorers often hide names that begin with a dot. The tabs below show how to reveal the folder on your operating system.

macOS

  • Select Finder in the Dock.
  • Return to the local celld project folder from earlier.
  • Press Command+Shift+. to show hidden files.
  • Open .celld.
  • Open dev.

Windows

  • Select File Explorer from the taskbar.
  • Enter \\wsl$ in the address bar.
  • Open the WSL distribution that contains your local celld project folder.
  • Return to the local celld project folder from earlier.

File Explorer is now showing the project through WSL. The hidden object store still needs to be revealed.

  • Select View in the File Explorer toolbar.
  • Select Show.
  • Select Hidden items.
  • Open .celld.
  • Open dev.

Linux

  • Select Files from the Activities overview.
  • Return to the local celld project folder from earlier.
  • Press Ctrl+H to show hidden files.
  • Open .celld.
  • Open dev.

You found the local store. The .celld/dev folder gives the persisted counter an on-disk home that you can inspect.

Can't find .celld?

  • Confirm that your file explorer is showing hidden files.
  • Check that you returned to the same local project folder used by the running development server.
  • Keep the restarted celld process running while you inspect the folder.

Help me locate the local object store.

Inspect the stored entries

The contents of .celld/dev are runtime data managed by celld. Their presence shows that development mode created local storage inside your project folder.

  • Confirm that .celld/dev contains at least one entry.
  • Leave every entry unchanged.

What does this prove?

The browser proved that the counter value survives a restart. The populated .celld/dev folder reveals where celld keeps that development state on disk.

This persistence chain stays on your laptop. No Cloudflare account or deployment holds the counter value.

Confirm the counter still runs

A final browser request proves that inspecting the store did not interrupt the running Worker. The counter should continue the same sequence from its persisted value.

  • Switch back to the counter page in your browser.

Before you refresh, what value do you expect to see next?

  • Refresh the counter page once.

You'll see the next count in the sequence. Its value is 1 higher than the value shown before the refresh.

  • Return to the open .celld/dev folder in your file explorer.

The local object store remains visible while the restarted development server handles requests. You have connected the browser counter to its persisted state on disk.

Counter not increasing?

  • Confirm that the restarted celld development process is still running in the terminal from earlier.
  • Check that your browser still uses the local address printed by that process.
  • Keep the .celld/dev folder intact while you diagnose the request.

Help me diagnose the counter.

Your local state now has a visible home. Next, you'll change the counter so each request adds ten.

Customize the Counter Increment

Your local object store has already proved that the Durable Object keeps its count across a restart. The counter still rises by 1 for every browser request.

Now you'll change the example so each request adds 10. A fresh celld process lets the browser prove that your own logic is running.

In this step, get ready to:
  • Locate the counter update in the example source.
  • Change the increment from 1 to 10.
  • Prove the customized counter increases in steps of 10.
Locate the counter update

The .celld/dev store holds the current value. The counter source decides how much each request adds.

Finding one numeric value in an example can be fiddly. Project-wide search narrows the results to the update inside the counter source.

  • Open the local project folder in your editor.
  • Search the source files for the counter update that adds 1 to the stored count.
  • Select the matching result in the counter example source.

You should land on the update that currently advances the stored count by 1 for each request.

Change the increment to 10

The numeric increment controls the size of each change. The Durable Object binding and storage logic stay unchanged.

  • Replace the numeric increment 1 with 10 in the counter update you found.
  • Save the edited counter source file.

Why keep the storage logic unchanged?

The existing count remains stored in .celld/dev. Only the amount added by each future request changes.

Restart and verify the counter

A restart gives the edited example a fresh process. The existing count remains available in .celld/dev.

  • Switch back to the terminal from earlier.
  • Stop the running development process by pressing Ctrl+C.
  • Start the customized Worker from the same local project folder by running this command:
celld dev

What does this command do?

The command starts the local Workers runtime for the current project. The Worker reads the persisted Durable Object state from .celld/dev.

  • Wait until the terminal prints the local address.
  • Return to the browser tab from earlier.
  • Read the count currently displayed before refreshing.

Before you refresh, decide whether the next request should add 1 or 10.

  • Refresh the browser once.

You'll see the count jump by 10 from the value you just read.

  • Refresh the browser two more times.

That's your customization running. Each refresh moves the persisted count forward by another 10.

Count still increasing by 1?

  • Confirm the edited source file is saved with an increment of 10.
  • Confirm you restarted the development process after saving the file.
  • Confirm the terminal is serving the same local project folder you edited.

Help me find why my local counter still increments by 1.

✔️ Awesome, I've got everything!

Your customized local counter is running. Every browser request now adds 10 to the persisted value.

ⓧ I'd like to double check the final state

  • Confirm celld remains installed locally.
  • Confirm the local project folder contains the customized counter example.
  • Confirm the Durable Object counter adds 10 to its existing count for each browser request.
  • Confirm the restarted development process serves the customized Worker from the local project folder.
  • Confirm the browser count increases in steps of 10.
  • Confirm .celld/dev remains inside the local project folder with the persisted state.
  • Confirm no Cloudflare account was used.
  • Confirm the Worker was not deployed.

Secret mission

Audit Local State Persistence

Run a controlled persistence audit around a celld restart. Compare recorded counter values to prove that the on-disk Durable Object state continues in steps of 10.

Clean Up Your Resources

Clean Up Your Resources

Everything in this project runs locally through celld, so there are no cloud resources or ongoing charges. Choose whether to keep testing, stop the local server, or reset the saved counter state.

Resources you used:

  • A local celld dev process serving your customized counter Worker.
  • A local .celld/dev object store containing the persisted Durable Object state.

Keep everything running

No action is needed. Choose this if you are still testing the customized counter.

  • Leave the celld dev process running while you continue testing.
  • Keep the .celld/dev folder to preserve the current counter value.
  • Keep the local project folder as your reusable counter app.

Pause - I'll come back to this later

Pausing frees the local resources used by the development server. Your source files and saved counter state remain available.

  • Switch back to the terminal running celld dev.
  • Press Ctrl+C to stop the process.
  • Refresh the counter page in your browser.
  • Keep the local project folder for your next session.

The counter page no longer returns a fresh value. Your saved count remains in .celld/dev.

Delete - I don't want to use this again

Deleting the local object store resets the persisted counter state. Your customized counter source stays in the local project folder.

  • Switch back to the terminal running celld dev if the process is still active.
  • Press Ctrl+C to stop the process.
  • Return to the local project folder from earlier in your file explorer.
  • Delete the .celld/dev folder.
  • Refresh the file explorer.
  • Keep the local project folder as your copy of the customized counter app.

You should no longer see .celld/dev inside the local project folder. The counter starts with fresh local state if you run the app again.

Nice Work!

Nice Work!

You did it! Your local celld counter now preserves Durable Object state across restarts while increasing the count by 10 per request.

What you learned:

  • Ran a Cloudflare Workers app on your laptop with celld. Reached its local response in your browser without a Cloudflare account.
  • Proved persistent state by restarting celld dev. Watched the counter continue from its previous value.
  • Inspected the local object store inside .celld/dev. Changed the counter to increase by 10 per request.
  • Secret Mission: Took on an optional challenge to extend your local Workers skills.

Ready to quiz yourself?