State keeps small values that stay put between requests and deploys: a counter, a lock, a flag, a rate-limit bucket. Every call goes to one place, in order, so two requests never both win. It runs on a Cloudflare Durable Object.
Use State when a value must be exact. A key-value store is faster to read from everywhere but is eventually consistent, so two requests that increment the same KV key can lose an update. State cannot.
State is available on every plan, on container apps and on SSR and hybrid apps.
Add State
- Open your app. On Overview, choose Add resource, then State.
- Enter a Name, such as
visits. - Choose Attach, then redeploy the app.
State is not created or shared like a bucket: it belongs to the app. Every State you add to one app shares the same set of keys. Adding two gives you two names for the same store. The data outlives deploys.
Use it from your code
State answers the same paths on every runtime:
| Request | Result |
|---|---|
GET / |
Lists up to 100 keys as {"keys": [...]}. |
GET /{key} |
The value as text. A missing key is a 404. |
PUT /{key} |
Stores the request body as text. |
DELETE /{key} |
Removes the key. |
POST /incr/{key} |
Adds one and returns the new number. A missing key starts at 0. |
Container apps
Call the private host. Copy the exact host from the State sheet's Connect tab. It looks like dply.my-app.visits.internal.
use Illuminate\Support\Facades\Http;
Http::withBody('hello', 'text/plain')->put('http://dply.my-app.visits.internal/greeting');
$value = Http::get('http://dply.my-app.visits.internal/greeting')->body();
$visits = (int) Http::post('http://dply.my-app.visits.internal/incr/visits')->body();
const host = 'http://dply.my-app.visits.internal';
await fetch(`${host}/greeting`, { method: 'PUT', body: 'hello' });
const value = await (await fetch(`${host}/greeting`)).text();
const visits = Number(await (await fetch(`${host}/incr/visits`, { method: 'POST' })).text());
Worker apps (SSR and hybrid)
Call env.NAME.fetch() with a path. The host in the URL does not matter.
export default {
async fetch(request, env) {
const n = await (await env.VISITS.fetch('https://state/incr/visits', { method: 'POST' })).text();
return new Response('Visit #' + n);
},
};
The raw Durable Object namespace is available as env.NAME.namespace if you need it.
Try it
State lives inside the running app and only the app can reach it, so the dashboard cannot read or write it. The sheet's Try it tab gives a command to run from the app instead:
curl -X POST http://dply.my-app.visits.internal/incr/visits
curl http://dply.my-app.visits.internal/
Pricing
State bills every unit from the first:
| Meter | Price | Unit |
|---|---|---|
| Requests | $0.195 | per million |
| Duration | $16.25 | per million GB-seconds |
| Rows read | $0.0013 | per million |
| Rows written | $1.30 | per million |
| Storage | $0.26 | per GB-month |
Duration is the time the object is active, at 128 MB. Storage is the largest size during the month. Usage is collected daily and billed to your organization. This bills at the invoice line Workers CPU, Durable Objects, object storage and images.
Note
On a container app, the Durable Object that runs the container itself is metered at these same rates, whether or not you add State. Its usage and State's are reported together.
Delete
Choose Delete on the card, or Delete State in the sheet. The app loses this State on the next deploy, and code that calls it gets errors.
The keys are not wiped. Every State on the app shares one store, so another State on the app, or one you add later, sees the same keys again. They are deleted only with the app.