---
image: true
author: nathan-flurry
published: "2025-03-23"
category: technical
keywords: ["durable-objects","rivetkit","standard","w3c"]
title: "Considering A W3C Standard For Stateful Serverless"
description: "**We're Rivet, a new open-source, self-hostable serverless platform with a focus on stateful serverless. If you want to support our mission of building a production-ready and self-hostable serverless runtime, give us a star on GitHub.**"
---

export const tweetImg = { src: "https://assets.rivet.dev/website/blog/2025-03-23-what-would-a-w3c-standard-look-like-for-stateful-serverless-/tweet.png", width: 2388, height: 2264, format: "png" };
export const serverlessworkerimg = { src: "https://assets.rivet.dev/website/blog/2025-03-23-what-would-a-w3c-standard-look-like-for-stateful-serverless-/serverlessworker.png", width: 1916, height: 1060, format: "png" };
export const sharedworkerimg = { src: "https://assets.rivet.dev/website/blog/2025-03-23-what-would-a-w3c-standard-look-like-for-stateful-serverless-/sharedworker.png", width: 1258, height: 934, format: "png" };

**We're Rivet, a new open-source, self-hostable serverless platform with a focus on stateful serverless. If you want to support our mission of building a production-ready and self-hostable serverless runtime, <a href="https://github.com/rivet-dev/rivet" target="_blank">give us a star on GitHub</a>.**

Earlier today, I was <a href="https://x.com/janwilmake/status/1903580372920590667" target="_blank">called upon</a> by Cloudflare's <a href="https://x.com/threepointone" target="_blank">@threepointone</a> and <a href="https://x.com/janwilmake" target="_blank">@janwilmake</a> regarding my thoughts on a standard for stateful serverless.

<a href="https://x.com/janwilmake/status/1903580372920590667" target="_blank"><img src={tweetImg.src} alt="threepointone tweet" /></a>

Having implemented a cloud-agnostic stateful serverless library called <a href="https://github.com/rivet-dev/rivet/tree/main/rivetkit-typescript/packages/rivetkit" target="_blank">RivetKit</a>, I have a lot of experience with what's required to build a portable standard. And thus, I derailed my Saturday to write them down here.

This specification is intended as step 1 to start the conversation about what is required to develop a stateful serverless specification that makes stateful serverless easier for companies to adopt. I don't expect this to be the end result. See _Specification Goals_ for more information.

## What Is Stateful Serverless?

Unless you're deep in to <a href="https://developers.cloudflare.com/durable-objects/" target="_blank">Cloudflare Durable Objects</a> or <a href="/docs/actors" target="_blank">Rivet Actors</a>, there's a good chance you've never heard of "stateful serverless."

**Stateful serverless** allows serverless functions to maintain state across multiple invocations. They're very similar to **Web Workers (specifically `SharedWorker`) on the server** and share characteristics with the **actor model**. The most popular implementation of this today is <a href="https://developers.cloudflare.com/durable-objects/" target="_blank">Durable Objects</a>.

The adoption of stateful serverless is fairly new: Durable Objects was announced <a href="https://blog.cloudflare.com/introducing-workers-durable-objects/" target="_blank">4.5 years ago</a> and only recently saw widespread use over the past couple years.

### Stateless Serverless (Functions) vs Stateful Serverless (Workers)

**Stateless serverless** is what you'd normally think of when you hear **"serverless functions."** An example implementation on Cloudflare Workers would look like this:

```typescript
export default {
  fetch(request, env) {
    return new Response('Hello World!');
  }
}
```

On the other hand, **stateful serverless provides an infinitely running process with storage**. An example implementation on Rivet Actors would look like this:

```typescript
// Once created, this DurableObject will exist indefinitely
export class Counter extends DurableObject {
  count: number;

  constructor(state, env) {
    super(state, env);
    this.setup();
  }

  async setup() {
    this.count = state.storage.get("count") || 0;
  }

  increment(count: number) {
    // Update state
    this.count += 1;

    // Persist state
    await this.ctx.storage.put("count", this.count);

    return this.count;
  }
}
```

### Primary Use Cases

The primary use cases of stateful serverless are:

* **Long-Running Processes**: Tasks that execute over extended periods or in multiple steps. For example, **AI Agents** with ongoing conversations and stateful tool calls.
* **Stateful Services**: Applications where maintaining state across interactions is critical. For example, **Collaborative Apps** with shared editing and automatic persistence.
* **Realtime Systems**: Applications requiring fast, in-memory state modifications or push updates to connected clients. For example, **Multiplayer Games** with game rooms and player state.
* **Durability**: Processes that must survive crashes and restarts without data loss. For example, **Durable Execution** workflows that continue after system restarts.
* **Horizontal Scalability**: Systems that need to scale by distributing load across many instances. For example, **Realtime Stream Processing** for stateful event handling.
* **Local-First Architecture**: Systems that synchronize state between offline clients. For example, **Local-First Sync** between devices.

Read more about stateful serverless <a href="https://rivet.dev/docs/actors" target="_blank">here</a>.

### Scope Of This Proposal

For the purpose of this article, we'll assume **stateful serverless includes message passing & persistent storage**. This means we're not going to consider actor runtimes, such as Erlang/OTP, Akka, Orleans, Actix, and Swift Actors which do not include storage as a core component.

## Why Should I Care?

Even if you've never heard of stateful serverless, you're probably using a site on a daily basis that already relies on stateful serverless with technologies like Cloudflare Durable Objects:

- CodePen
- Liveblocks
- Clerk
- Wordware
- Playroom
- Tldraw

Similarly, a significant portion of applications in general are powered by services with the actor pattern, which is a subset of what this standard would provide:

- WhatsApp (notoriously acquired for $19B with Erlang/OTP having only 35 engineers)
- Discord
- LinkedIn
- Twitter/X
- Pinterest
- PayPal
- Halo
- FoundationDB (powering Apple, Snowflake, DataDog)
- Many, many more

## Why Build A Web Standard?

### Customers Wary Of Vendor-Lock

In 2025, customers are **wary of vendor-locking themselves to cloud services**. It's common for vendor-locked providers to either shut down, turn out to be unreliable, or price gouge contracts because their customers cannot leave.

Therefore, a standard allows all platforms offering stateful serverless to become a compelling offering. It's a rising tide: more competition means wider adoption and more customers.

For example: though AWS is the leading cloud provider and pushes their own closed-source & non-standard software like DynamoDB & Lambda, **they still need to provide standard- and open-source-compatible software** like Redis (ElastiCache), Cassandra (Keyspace), and Postgres/MySQL (Aurora) in order to remain an attractive offering for customers concerned about vendor lock.

### Incentivize More Providers

**A standard would incentivize more cloud providers to enter the stateful serverless space.** Currently, only Cloudflare, Rivet, and RivetKit offer this capability, leaving a massive opportunity for other serverless cloud providers to adopt stateful serverless. With a common standard, more frameworks & developers can adopt this model.

## Today's Stateful Serverless Implementations

### Cloudflare Durable Objects: The Most Popular Option

Today, [Cloudflare Durable Objects](https://developers.cloudflare.com/durable-objects/) are the incumbent of stateful serverless cloud providers.

A Durable Object looks something like this:

```typescript
export class Counter extends DurableObject {
  constructor(state, env) {
    super(state, env);
    this.setup();
  }

  async setup() {
    this.count = state.storage.get("count") || 0;
  }

  increment(count: number) {
    this.count += 1;
    await this.ctx.storage.put("count", this.count);
    return this.count;
  }
}
```

### Cloudflare Agents Framework: Moving Beyond Durable Objects

The [Cloudflare Agents](https://developers.cloudflare.com/agents/) framework is for AI-powered agents to operate on the Durable Objects infrastructure.

As stated in their [launch blog post](https://blog.cloudflare.com/build-ai-agents-on-cloudflare/):

> Over the coming weeks, expect to see ... the ability to self-host agents on your own infrastructure.

Purely theorizing: a standard for stateful serverless might provide the foundation for this.


### Rivet: Open-Source Serverless Infrastructure

I'm the founder of Rivet and have a vested interest in seeing stateful serverless become a standard. Rivet provides an open-source stateful serverless platform that can be easily self-hosted.

We also provide a handful of features in our runtime that don't make sense to be part of a standard, since the best W3C standards build on existing web standards: Docker containers for non-JavaScript applications, HTTP, UDP, and TCP support, advanced lifecycle management, actor tagging for advanced multi-tenant applications, advanced control over actor upgrades for companies with specific use cases, fine-grained control over where your actor is running, and a developer-friendly REST API for managing Rivet Actors.

We encourage developers building on Rivet to use [RivetKit](https://github.com/rivet-dev/rivet/tree/main/rivetkit-typescript/packages/rivetkit). However, we also provide a low-level API for defining actors:

```typescript
import type { ActorContext } from "@rivet-gg/actor";
import * as http from "http";

export default {
  async start(ctx: ActorContext) {
    let count = ctx.kv.get("count") || 0;
    const server = http.createServer((req, res) => {
      count += 1;
      await ctx.kv.put("count", count);

      res.writeHead(200, { "Content-Type": "text/plain" });
      res.end(`Count: ${this.count}`);
    });
    server.listen(process.env.PORT_HTTP);
    
    // Keep the actor running until explicitly destroyed
    await new Promise((resolve) => {});
  }
};
```

Unlike Durable Objects, Rivet's core API design opts to lean into Unix-like patterns such as using `SIGINT` signals, leveraging `process.exit`, and using environment variables.

Check out the [source code on GitHub](https://github.com/rivet-dev/rivet).

### RivetKit Framework: Stateful Serverless On Any Cloud

At Rivet, we also manage a framework called [RivetKit](https://github.com/rivet-dev/rivet/tree/main/rivetkit-typescript/packages/rivetkit) that provides a Durable Object-like experience on any cloud that provides _stateless_ serverless.

A Rivet Actor definition looks something like this:

```typescript
import { actor } from "rivetkit";

const chatRoom = actor({
  state: { messages: [] },
  actions: {
    // receive an action call from the client
    sendMessage: (c, username, message) => {
      // save message to persistent storage
      c.state.messages.push({ username, message });

      // broadcast message to all clients
      c.broadcast("newMessage", username, message);
    },
    // allow client to request message history
    getMessages: (c) => c.state.messages
  }
});
```

Under the hood is where it gets interesting: RivetKit is intended to support as many platforms as possible and is **completely abstracted with drivers**. By doing so, RivetKit supports Rivet, Cloudflare Durable Objects, Bun, Node.js. Vercel, Supabase, Deno, and Lambda are coming in the near future.

At the time, these internal drivers provided a **peek at what a standard might require**, since they were abstracted to be platform-agnostic:

**ActorDriver**: Managed actor state, lifecycle, and persistence

```typescript
export interface ActorDriver {
	kvGet(actorId: string, key: KvKey): Promise<KvValue | undefined>;
	kvGetBatch(actorId: string, key: KvKey[]): Promise<(KvValue | undefined)[]>;
	kvPut(actorId: string, key: KvKey, value: KvValue): Promise<void>;
	kvPutBatch(actorId: string, key: [KvKey, KvValue][]): Promise<void>;
	kvDelete(actorId: string, key: KvKey): Promise<void>;
	kvDeleteBatch(actorId: string, key: KvKey[]): Promise<void>;
	setAlarm(actor: AnyActorInstance, timestamp: number): Promise<void>;
}
```

**ManagerDriver**: Handled actor discovery, routing, and scaling

```typescript
export interface ManagerDriver {
	getForId(input: GetForIdInput): Promise<GetActorOutput | undefined>;
	getWithTags(input: GetWithTagsInput): Promise<GetActorOutput | undefined>;
	createActor(input: CreateActorInput): Promise<CreateActorOutput>;
}

export interface GetForIdInput<E extends Env = any> {
	c?: HonoContext<E>;
	baseUrl: string;
	actorId: string;
}

export interface GetWithTagsInput<E extends Env = any> {
	c?: HonoContext<E>;
	baseUrl: string;
	name: string;
	tags: ActorTags;
}

export interface GetActorOutput<E extends Env = any> {
	c?: HonoContext<E>;
	endpoint: string;
	name: string;
	tags: ActorTags;
}

export interface CreateActorInput<E extends Env = any> {
	c?: HonoContext<E>;
	baseUrl: string;
	name: string;
	tags: ActorTags;
	region?: string;
}

export interface CreateActorOutput {
	endpoint: string;
}
```

There was also a coordinated topology for implementing actors over peer-to-peer for platforms that do not natively support stateful serverless. This would not apply to an actor standard.

Read more about the framework in the [Rivet repository](https://github.com/rivet-dev/rivet/tree/main/rivetkit-typescript/packages/rivetkit).

Check out the [source code on GitHub](https://github.com/rivet-dev/rivet).

### Other Actor Runtimes: The Precursor To Stateful Serverless

Runtimes like OTP (i.e. Erlang & Elixir & Gleam), Akka, Orleans, Actix, and Swift Actors all **lack built-in support for persisted state**. There are libraries that provide state for actors, but their models are significantly different than what we'll consider here. Additionally, we're focused on JavaScript since that's primary language for web standards.

## Specification Goals

For the purpose of this exploration, I'm going to **recommend sticking with web standards**, which are managed by the <a href="https://www.w3.org/" target="_blank">W3C</a> organization. Doing so allows us to **build on top of existing successful web standards**, such as <a href="https://wintertc.org/" target="_blank">WinterTC</a> and the <a href="https://fetch.spec.whatwg.org/#request-class" target="_blank">fetch specification</a> that are currently offered across almost all stateless serverless platforms already.

The goals of this proposal are:

- **Simplicity Of Implementation**: Implementing standards correctly is difficult, expensive, and highly error prone. Simpler is better.
- **Simplicity Of Usage**: Stateful serverless is not a concept that is easy for many developers to understand. Prefer simplicity vs advanced features to keep the API as simple as possible for greater adoption.
- **Build For Frameworks**: <a href="https://hono.dev/" target="_blank">Hono</a> and <a href="https://itty.dev/itty-router/" target="_blank">itty-router</a> already proved that frameworks are almost always used with serverless runtimes. Stateful serverless is already used widely with <a href="https://www.partykit.io/" target="_blank">PartyKit</a>, <a href="https://github.com/rivet-dev/rivet/tree/main/rivetkit-typescript/packages/rivetkit" target="_blank">RivetKit</a>, <a href="https://developers.cloudflare.com/agents/" target="_blank">Agents</a>, and misc tools like <a href="https://tinybase.org/" target="_blank">TinyBase</a>. If possible, leave functionality up to frameworks to implement.
- **Built On Existing Web Development Paradigms**: Stateful serverless is very similar to <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API" target="_blank">Web Workers</a>. Where possible refer to Web Workers to see what works well.
- **Focus On Stateful Workloads, Not Actors**: I love building distributed systems with the actor model, but actors require extra consideration around message handling, concurrency, and backpressure.

### Classes vs Functions

Durable Objects provides a class-based approach to stateful serverless (e.g. `class X extends DurableObject { ... }`). Rivet provides a functional approach (e.g. `export default { start () { ... } }`).

Sticking to the WinterTC ESM-style makes sense. Additionally, it's similar to how Web Workers are already defined.

### Alternative #1: Fetch-Based Communication

Both Rivet & Durable Objects use WinterTC-like `fetch` handlers to accept requests on Durable Objects/Rivet Actors. An alternative spec would opt to use `fetch` handlers instead of a brand new definition. This is similar to what WinterTC is attempting to do: take existing patterns and standardize them as opposed to implementing a new standard.

This standard makes an intentional decision to define a simpler, more portable API than provide compatibility with only 2 existing implementations.

### Alternative #2: A CGI-Like Standard

An alternative implementation is to consider something along the lines of [CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface) which provides a more Unix-like implementation of serverless functions & supports multiple languages. However, the industry has consolidated behind JavaScript & WebAssembly for serverless clouds because of the developer & cost efficiency. Additionally, CGI lacks many of the modern features that W3C specifications take into account for web standards. (Originally proposed by [Armin Ronacher](https://x.com/mitsuhiko).)

## Drawing Inspiration From `SharedWorker`

<img src={sharedworkerimg.src} alt="Overview of SharedWorker arch" />

Though designed for client-side concurrency, [`SharedWorker`](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker) offers a programming model that closely resembles what we're discussing for server-side actors. The `SharedWorker` API already has many of the core concepts needed for a stateful serverless standard:

1. **Multi-Tenant**: `SharedWorker` can serve multiple browser tabs, similar to how Durable Objects and Rivet Actors serves multiple clients
2. **Isolated Execution Context**: Each worker runs in its own isolated environment
2. **Message-Based Communication**: Workers communicate via structured message passing.

You can think of a `ServerlessWorker` like a [`SharedWorker`](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker) that is:

- In the cloud
- Includes persistent storage
- Runs forever

Here's a comparison of how Web Workers operate in the browser:

<CodeGroup>

```typescript {{ "title": "SharedWorker" }}
// main.js - Browser
const worker = new Worker('worker.js');
worker.postMessage({ type: 'INCREMENT', value: 5 });
worker.onmessage = event => console.log('Counter:', event.data);

// worker.js - Worker
let counter = 0;
self.onmessage = event => {
  const { type, value } = event.data;
  if (type === 'INCREMENT') {
    counter += value;
    self.postMessage(counter);
  }
};
```

```typescript {{ "title": "DurableObject" }}
// server.js - Fetch handler
const id = env.COUNTER.idFromName('counter-1');
const stub = env.COUNTER.get(id);
const response = await stub.fetch('https://counter', {
  method: 'POST',
  body: JSON.stringify({ type: 'INCREMENT', value: 5 })
});
const counter = await response.json();
console.log('Counter:', counter);

// counter.js - Durable Object
export class Counter {
  constructor(state) {
    this.state = state;
    this.counter = 0;
  }

  async fetch(request) {
    const { type, value } = await request.json();
    if (type === 'INCREMENT') {
      this.counter += value;
      await this.state.storage.put('counter', this.counter);
      return new Response(JSON.stringify(this.counter));
    }
    return new Response('Invalid request', { status: 400 });
  }
}
```

```typescript {{ "title": "RivetKit" }}
// client.js - Client side
import { createClient } from 'rivetkit';

const client = createClient();
const counter = await client.counter.get('counter-1');
const result = await counter.increment(5);
console.log('Counter:', result);

// counter.js - Actor definition
import { actor } from 'rivetkit';

export const counter = actor({
  state: { value: 0 },
  actions: {
    increment: (ctx, value) => {
      ctx.state.value += value;
      return ctx.state.value;
    }
  }
});
```

</CodeGroup>

The `onmessage` handler is very similar to the `fetch` handler in a Durable Object or an [Action](/docs/actors/actions) in RivetKit.

The key differences are:

1. **Persistence**: Web Workers lose state when the page refreshes, while `ServerlessWorker`s maintain state between invocations.
2. **Network Access**: Stateful serverless inherently supports network requests via fetch/WebSockets.
3. **Lifecycle Management**: `ServerlessWorker`s have advanced wake/sleep and scheduling capabilities.

## Introducing `ServerlessWorker`

<img src={serverlessworkerimg.src} alt="Overview of ServerlessWorker arch" />

`ServerlessWorker` is an API similar to the existing Web Workers API that provides the core functionality of both Cloudflare Durable Objects and Rivet Actors:

1. **Multi-Tenant**: `ServerlessWorker` can serve multiple clients at the same time in realtime.
2. **Message-Based Communication**: Communicates via `postMessage`.
3. **Persistent**: `ServerlessWorkers` run forever. They cannot crash. Implementations of the standard can optionally make workers sleep when there is no activity.
4. **Storage**: `ServerlessWorkers` can store data that persists for ever. Implementations of the standard should design this data to be stored next to compute for optimal read-write performance.
5. **Isolated Execution Context**: Each worker runs in its own isolated environment

### Creating, Initializing, & Addressing `ServerlessWorker`s

**Current Implementation: Rivet & RivetKit**

Rivet and RivetKit provide a tagging system for organizing actors. This allows you to build more manageable multi-tenant applications with complex requirements. For example:

Rivet allows you to create actors with the `actors.create` endpoint. The API accepts environment variables to configure how the actor behaves.

RivetKit allows passing tags to [`client.get({ /* tags */ })`](https://rivet.dev/docs/actors/keys) to get or create an actor, but does not provide environment variables. You can also use the `client.create` method to create a fresh actor. Actors can read their own tags to configure behavior accordingly:

```typescript
const client = createClient<App>("http://localhost:6420");
const randomChannel = await client.chatRoom.get({ organization: "rivet", channel: "random" });
```

**Current Implementation: Durable Objects**

To create a Durable Object on Cloudflare, you call the [`newUniqueId`](https://developers.cloudflare.com/durable-objects/api/namespace/#newuniqueid) method. This will give you a unique ID for a new Durable Object.

Cloudflare also provides a [`idFromName`](https://developers.cloudflare.com/durable-objects/api/namespace/#idfromname) method to get the ID of an Durable Object from an arbitrary string. This ID is then used to resolve a Durable Object stub that you can send requests to. For example:

```typescript
const id = env.MY_DURABLE_OBJECT.idFromName("channel:rivet:random");
const stub = env.MY_DURABLE_OBJECT.get(id);
await stub.fetch(...);
```

Cloudflare Durable Objects is desigend to have no concept of "created" or "destroyed." The initial state can be configured by sending an RPC to the DO upon creation. This implementation of not having an input state for a `ServerlessWorker` is simpler and leaves the functionality up to the framework; instead all a `ServerlessWorker` has a unique ID that developers can send messages to.

**Current Implementation: `SharedWorker`**

`SharedWorker` accepts a `name` parameter, for example: `new SharedWorker("./sharedWorker.js", `chat-room-random`)`. This is akin to calling `idFromName` for Durable Objects. Passing no name will give you the same worker.

The `name` is accessible in the global scope of the `SharedWorker`.

**Proposal**

Constructing a `ServerlessWorker` object will mimic the `SharedWorker` API. Passing a `name` parameter to the second constructor argument will let you address a given `ServerlessWorker`. Passing no arguments will give you the same worker.

The `name` is accessible in the global scope of the `ServerlessWorker`.

An example API could look like:

```typescript {{ "title": "server.js" }}
export default {
	fetch(req) {
		const worker = new ServerlessWorker("./worker.js");
		// ...
	}
}
```

To resolve with a custom name:

```typescript {{ "title": "server.js" }}
export default {
	fetch(req) {
		// ...
		const worker = new ServerlessWorker("./worker.js", `chat-room-${random}`);
		// ...
	}
}
```

To access the name in the worker:

```typescript {{ "title": "worker.js" }}
// Worker code can access its name
console.log(self.name);
```

### Terminating `ServerlessWorker`s

**Current Implementation: Rivet**

Rivet provides two ways to destroy an actor: use `process.exit(0)` or call `actors.destroy` from the API.

RivetKit will provide a `ctx.shutdown` method that delegates to `ActorDriver`, though is not currently implemented.

**Current Implementation: Durable Objects**

Cloudflare's approach to destroying Durable Objects is somewhat unique. There is no concept of "created" or "destroyed" actor. To reset an actor, you clear the storage with [`deleteAll()`](https://developers.cloudflare.com/durable-objects/api/storage-api/#deleteall). While unintuitive, this leaves it up to the framework to implement.

**Current Implementation: `SharedWorker`**

Web Workers' `SharedWorker` is automatically destroyed when all ports are closed. This is not relevant to stateful serverless.

**Proposal**

Add a `terminate` method on the `ServerlessWorker` itself, leave it up to the frameworks to implement destroying. For example:

```typescript {{ "title": "worker.js" }}
export default {
	connect(event) {
		self.terminate();
	}
}
```

### Connections & Messages

**Current Implementation: Rivet**

Both Rivet Actors & Durable Objects provide a way to serve requests to actors based on web standards.

Rivet provides flexible networking infrastructure, including UDP, TCP, and host networking. To remain widely compatible, Rivet relies on the Deno implementation of the fetch handler with [`Deno.serve`](https://docs.deno.com/api/deno/~/Deno.serve). 

**Current Implementation: Cloudflare**

Durable Objects supports a similar [fetch handler](https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/#invoking-the-fetch-handler), but also provides non-standard features like [RPC](https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/).

Both rely on different implementations for WebSockets, though this handled gracefully by libraries like [Hono's WebSocket helper](https://hono.dev/docs/helpers/websocket).

There is a WinterTC proposal for a [Sockets API](https://github.com/wintercg/proposal-sockets-api), though I'm going to consider this out of the scope of this article since [Cloudflare's implementation](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/) is not currently supported on Durable Objects.

**Current Implementation: `SharedWorker`**

However, Web Worker (`SharedWorker`) opts for using `onconnect` & `port.onmessage` like this:

<CodeGroup>

```typescript {{ "title": "main.js" }}
const sharedWorker = new SharedWorker('sharedWorker.js');
sharedWorker.port.postMessage({ action: 'greet', data: 'Hello from Tab' });
sharedWorker.port.onmessage = function(event) {
    console.log('Received response from SharedWorker:', event.data);
};
sharedWorker.port.start();
```

```typescript {{ "title": "sharedWorker.js" }}
onconnect = (event) => {
    const port = event.ports[0];

    port.onmessage = (event) => {
        const { action, data } = event.data;

        if (action === 'greet') {
            port.postMessage({ response: `SharedWorker received: ${data}` });
        }
    };
};

```
</CodeGroup>

**Proposal**

This could go two ways: lean into the existing [WinterTC for ESM-style handlers](https://github.com/wintercg/proposal-functions-api/tree/main) like Durable Objects and Rivet or opt for a simpler & portable interface like Web Workers.

In my opinion, request/response should use ESM-style exports with the `SharedWorker` interface. This allows the simplicity of request/response & also enables full bidirectional streaming -- like a WebSocket but without the overhead.

<CodeGroup>
```typescript {{ "title": "server.js" }}
export default {
	fetch(conn) {
		const workerId = createServerlessWorkerId();
		const worker = new ServerlessWorker("./worker.js", workerId);
		conn.port.postMessage({ action: 'greet', data: 'foo' });
	}
};
```

```typescript {{ "title": "server.js" }}
export default {
	connect(conn) {
		const port = conn.ports[0];

		port.onmessage = event => {
			const { action, data } = event.data;

			if (action === 'greet') {
				conn.postMessage({ response: `Stateful serverless received: ${data}` });
			}
		};

		port.onclose = event => console.log('Conn disconnected');
	}
};
```
</CodeGroup>

Coincidentally, this also feels very similar to socket.io whose API has proven itself as flexible & easy to understand.

### Storage

**Current Implementations: Rivet & Durable Objects & Deno**

Both Rivet and Durable Objects support a raw KV interface to access data stored on the actor itself.

While not specific to actors, Deno attempts to implement an [abstract KV interface](https://docs.deno.com/api/deno/~/Deno.Kv). However, this API is very opinionated to Deno Deploy and is by no means compatible across multiple clouds.

**Why KV**

Anything more (especially `IndexedDB`) is too complicated to expect cloud providers to implement correctly. `localStorage` has proven itself a simple & reliable key-value store that's lasted the test of time.

Frameworks can implement more advanced logic on top of key-value stores, just as they've done in the browser with `localStorage`.

**Async**

Regardless of what API this results in, it should be asynchronous. Cloudflare has gone as far as removing async code for their SQLite interface because they state it's fast enough to not require an async context; the KV API could likely be made as simple as something like the browser's `localStorage` by making each key a synchronous get. However, platforms all implement KV differently, so this may not make sense if attempting to provide maximum compatibility. Additionally, it would be impossible for compatibility layers to implement a KV API with an external medium if that medum requires an async operation to communicate with.

**Strings vs Structured Clone**

`localStorage` currently only supports strings, which is what most developers are used to. 

However, both Rivet & Cloudflare Workers support natively storing data types that implement [structured cloning](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) which allows storing many types of data in a compact V8-compatible binary interface with high-performance serialization & deserialization.

If structured cloning was a protocol defined earlier, `localStorage` would likely support it natively. Whatever storage mechanism is chosen should support structured cloning.

**A Word On Complexity**

W3C has a history of inventing incredibly complicated standards for storage that are both (a) hard to understand as developers and (b) difficult to implement as a platform.

If you know how to use the [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) API from memory, I'm impressed.

Additionally, [Web SQL](https://developer.chrome.com/blog/deprecating-web-sql) was an alternative standard that was deprecated because of its complexity.

Whatever the storage mechanism for `ServerlessWorker`s is, **keep it simple, easy to implement, and easy to understand**. Frameworks can always do the heavy lifting for you.

**Concurrency & Input/Output Gates**

Concurrency is difficult to implement correctly across multiple platforms.

Cloudflare Durable Objects intelligently prevents common storage race conditions on Durable Objects without transactions through a mechanism called _input/output gates_ detailed [here](https://blog.cloudflare.com/durable-objects-easy-fast-correct-choose-three/#our-answer-make-it-automatic). This can be disabled with `ctx.storage.get(/* key*/, { allowConcurrency: true })`. Doing so will make the KV work like naive get/set without any extra functionality.

This is a controversial opinion, but I think that this behavior should be disabled by default and not part of a specification for a few reasons:

1. Durable Object users almost never read from storage at runtime. `storage.get` is called in the constructor and `storage.put` is called when changed.
2. This behavior can be implemented at the framework level.
3. This behavior is almost certainly going to be incorrectly implemented by vendors.
4. Input/output gates do not work with WebSocket on Durable Objects already (please correct me if I'm wrong) and most developers aren't accounting for this. `postMessage` and `onmessage` are closer to the Durable Object WebSocket behavior than the RPC behavior.

**Keeping Scope Small**

For the purpose of building a broader picture of what `ServerlessWorker` might look like in practice, I'm going to assume that storage is going to be part of this specification. However, I think it makes more sense to either (a) find an existing mature spec to use for storage or (b) define a separate spec that can work in contexts outside of `ServerlessWorker`.

**Proposal**

Provide a dead simple async structured clone-based KV API and let the platforms provide extra configs for concurrency. For example:

```typescript {{ "title": "worker.js" }}
let count = await storage.get("count") || 0;

export default {
  connect(conn) {
	const port = conn.ports[0];

    port.onmessage = async (event) => {
      count += 1;
      await storage.put("count", count);
    };
  }
}
```

### Scheduling

**Current Implementations**

A core part of `ServerlessWorker`s is to be able to run a function at an arbitrary timestamp.

Cloudflare provides a simple `setAlarm` API that has a single alarm. If you already have an alarm, it will override that alarm. I like this API since it's significantly simpler than `setInterval` and `setTimeout` and lets frameworks provide more advanced APIs, such as CRONs.

**Proposal**

Provide an API similar to Cloudflare's [`setAlarm`](https://developers.cloudflare.com/durable-objects/api/storage-api/#setalarm). For example:

```typescript {{ "title": "worker.js" }}
// Set an alarm for 1 day
setAlarm(Date.now() + 86_400 * 1000);

export default {
	alarm() {
		console.log("It's been 1 day");
	}
}
```

### Sleeping & Upgrading & Host Migrations

A core advantage of `ServerlessWorker`s to something like [Kubernetes jobs](https://kubernetes.io/docs/concepts/workloads/controllers/job/) or other container-based stateful workloads: `ServerlessWorker`s can **automatically sleep when there are no active operations** and **wake upon either fetch or alarm**.

However, `ServerlessWorker` has to address specific edge cases when there are tasks that need to run in the background before being put to sleep.

**Current Implementation: Rivet**

Rivet mimics Unix behavior by sending a `SIGINT` signal to the process when it plans to go to sleep. Your application is given time to clean up any work and restart gracefully when it's ready. This provides compatibility for a wide range of existing frameworks that implement the Node.js or Deno shutdown handler.

**Current Implementation: Durable Objects**

Durable Objects provides [`waitUntil`](https://developers.cloudflare.com/durable-objects/api/state/#waituntil). My understanding is this is effectively a noop since ([see note](https://developers.cloudflare.com/durable-objects/api/state/#waituntil)). However, I like the `waitUntil` API since it provides a JavaScript-y way of forcing the actor to stay awake.

**Proposal**

Provide an API similar to Durable Objects `waitUntil`. The provider can decide the maximum duration for this function. For example:

```typescript {{ "title": "worker.js" }}
async function doSomethingAsync() {
	// ...
}

waitUntil(doSomethingAsync());
```

### Handling Backpressure

Backpressure is a core problem in every distributed system and should always be handled carefully by developers to handle load gracefully.

**Current Implementation: Rivet**

Rivet provides low-level primitives to the developer, so it's up to the developer to handle backpressure accordingly. An overloaded actor without any special backpressure handling will saturate the CPU.

**Current Implementation: Durable Objects**

Durable Objects throw an error with the `.overloaded` property set to `true` when making requests to a Durable Object that cannot accept more requests.

**Proposal**

Define an error type that can be used by developers to handle backpressure gracefully.

### Security Model

**Proposal**

`ServerlessWorker` should rely on the existing work in WinterTC. They are secured via these attributes:

- **Fetch Handler**: Stateful `ServerlessWorker`s are only accessible through the stateless serverless fetch handler defined in WinterTC. This creates a protected gateway where all requests are authenticated and authorized before reaching any `ServerlessWorker`.
- **Isolation Boundaries**: Each `ServerlessWorker` runs in its own isolated environment, similar to Web Workers, preventing cross-worker data access without explicit communication channels.
- **No Direct Network Access**: `ServerlessWorker`s cannot be directly addressed from the public internet. All communication flows through the fetch handler, which can implement rate limiting, validation, and other security controls.

The downside of relying on the fetch handler is that it may restrict who can implement `ServerlessWorker`. For example, there are many container orchestration platforms that may be able to implement the worker part of `ServerlessWorker`, but cannot implement the `fetch` handler. I don't think this is an issue, since including these technologies that don't already comply to WinterTC would make it too broad to be effective.

### Out Of Scope

Items out of the scope for this spec:

- **Locality**: Cloudflare & Rivet both have notions of where actors run, though the default is to run nearest where the request is coming from. This will likely end up as a platform-specific feature when constructing `ServerlessWorker`.
- **Supervisors**: Traditional actor runtimes use "supervisors" to automatically restart crashed actors. `ServerlessWorker` cannot crash, and instead will automatically log and disregard thrown errors -- similar to service workers.
- **SQLite Storage**: Many serious use cases of Durable Objects rely on SQLite. This is out of the scope for this web standard.
- **Versioning**: Cloudflare and Rivet each have significantly different ways of implementing versioning. Rivet is more like AWS AMI, while Cloudflare uses a simpler but less flexible version history.
- **Upgrades & Rolling Deploys**: Leave it up to the platform to decide what code is running. Under the hood, the Rivet implementation of this standard would automatically call `actors.upgrade`
- **Logging**: Leave this up to the platform to decide how to ship logs.

## Full Examples

### Rate Limiter

This example demonstrates building a rate limit.

<CodeGroup>
```typescript {{ "title": "rateLimiter.js" }}
// 10 requests/minute
const maxRequests = 10;
const bucketDuration = 60_000;

// Load state from storage
let count = await storage.get("count") || 0;
let resetAt = await storage.get("resetAt") || (Date.now() + bucketDuration);

function saveState() {
  await storage.put("count", count);
  await storage.put("resetAt", resetAt);
}

export default {
  connect(conn) {
    const port = conn.ports[0];

    port.onmessage = (event) => {
      const { type, ...data } = event.data;
      
      // Check if bucket should be reset
      const now = Date.now();
      if (now >= resetAt) {
        count = 0;
        resetAt = now + bucketDuration;
        saveState();
      }

      switch (type) {
        // Check if within rate limit
        case "HIT":
          // Update limit
          const isAllowed = count < maxRequests;
          if (isAllowed) {
            count += 1;
            saveState();
          }

          conn.postMessage({ isAllowed });

          break;

        // Reset rate limit (unused, purely for example)
        case "RESET":
          count = 0;
          resetAt = now + bucketDuration;
          saveState();
          break;
      }
    };
  }
}
```

```typescript {{ "title": "server.js" }}
export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    // Get client IP
    const forwardedFor = request.headers.get('x-forwarded-for') || '127.0.0.1';
    const clientIp = forwardedFor.split(',')[0].trim();
    
    // Create a rate limiter for this IP
    const limiterId = `rate-limit:${ip}`;
    const worker = new ServerlessWorker("./rateLimiter.js", limiterId);

    // Handle response
    const { promise, resolve, reject } = Promise.withResolvers();
    worker.port.onmessage = (event) => {
      const { isAllowed } = event.data;
      
      if (isAllowed) {
        // Request is allowed - continue to handle the actual request
        resolve(new Response("Request allowed"));
      } else {
        // Request is rate limited
        resolve(new Response("Rate limit exceeded", { status: 429 }));
      }
    };

    // Send the rate limit check
    worker.port.postMessage({ type: "HIT" });

    return await promise;
  }
}
```
</CodeGroup>

### Pub/Sub Server

This example demonstrates a simple pub/sub system where each topic is its own serverless worker with connected clients.

<CodeGroup>
```typescript {{ "title": "topicWorker.js" }}
// A set of all connected clients
const subscribers = new Set();

export default {
  connect(event) {
    const port = event.ports[0];
    subscribers.add(port);
    
    port.onmessage = event => {
      const { type, data } = event.data;
      
      if (type === "publish") {
        // Broadcast to all subscribers
        for (const client of subscribers) {
          client.postMessage({
            type: "message",
            data
          });
        }
      }
    };
    
    // Remove on disconnect
    port.onclose = () => {
      subscribers.delete(port);
    };
  }
}
```

```typescript {{ "title": "server.js" }}
export default {
  async fetch(request) {
    const url = new URL(request.url);
    const [_, topic] = url.pathname.split('/');
    
    if (!topic) {
      return new Response("Specify a topic in the URL path");
    }
    
    // Each topic gets its own worker
    const worker = new ServerlessWorker("./topicWorker.js", `topic-${topic}`);
    
    // Handle WebSocket upgrade
    if (request.headers.get("Upgrade") === "websocket") {
      const pair = new WebSocketPair();
      const client = pair[0];
      const server = pair[1];
      
      // Connect to worker
      const port = worker.port;
      
      // Client -> Worker
      server.onmessage = event => {
        try {
          port.postMessage(JSON.parse(event.data));
        } catch (err) {
          server.send(JSON.stringify({ error: "Invalid JSON" }));
        }
      };
      
      // Worker -> Client
      port.onmessage = event => {
        server.send(JSON.stringify(event.data));
      };
      
      server.accept();
      
      return new Response(null, {
        status: 101,
        webSocket: client
      });
    }
    
    return new Response("Expected WebSocket");
  }
}
```

```javascript {{ "title": "client.js" }}
// Subscribe to a topic
const ws = new WebSocket("wss://pubsub.example.com/news");

// Handle incoming messages
ws.onmessage = event => {
  const { type, data } = JSON.parse(event.data);
  
  if (type === "message") {
    console.log("New message:", data);
  }
};

// Publish a message to the topic
function publish(data) {
  ws.send(JSON.stringify({
    type: "publish",
    data
  }));
}

// Example usage
publish("Breaking news!");
```
</CodeGroup>

### Simple Chat Room

This example demonstrates a simple chat room implementation with message persistence.

<CodeGroup>
```typescript {{ "title": "chatRoom.js" }}
// Keep track of all connections to this chat room
const connections = new Set();

// Keep messages in memory
let messages = await storage.get("messages") ?? [];

// Broadcast a message to all connected clients
function broadcast(message) {
  for (const conn of connections) {
    conn.postMessage(message);
  }
}

export default {
  connect(event) {
    const port = event.ports[0];
    connections.add(port);
    
    // Send message history to new client
    if (messages.length > 0) {
      port.postMessage({
        type: "history",
        messages: messages.slice(-20)
      });
    }
    
    port.onmessage = async (event) => {
      const { text, username } = event.data;
      
      // Create message object
      const message = {
        text,
        username,
        timestamp: Date.now()
      };
      
      // Store message
      messages.push(message);
      
      // Save periodically 
      if (messages.length % 10 === 0) {
        await storage.put("messages", messages);
      }
      
      // Broadcast to all clients
      broadcast({
        type: "message",
        message
      });
    };
    
    // Handle client disconnection
    port.onclose = () => {
      connections.delete(port);
      
      // If this was the last connection, save messages
      if (connections.size === 0) {
        waitUntil(storage.put("messages", messages));
      }
    };
  }
}
```

```typescript {{ "title": "server.js" }}
export default {
  async fetch(request) {
    const url = new URL(request.url);
    
    // Get chat room name from URL
    const [_, roomId] = url.pathname.split('/');
    
    if (!roomId) {
      return new Response("Please specify a room ID in the URL path", { status: 400 });
    }
    
    // Get or create a worker for this room
    const worker = new ServerlessWorker("./chatRoom.js", `chat-${roomId}`);
    
    // Handle websocket upgrade
    if (request.headers.get('Upgrade') !== 'websocket') {
      return new Response('Expected websocket', { status: 426 });
    }

    // Create a WebSocket pair
    const pair = new WebSocketPair();
    const client = pair[0];
    const server = pair[1];
    
    // Connect client and worker
    const port = worker.port;
    
    // Client -> Worker
    server.onmessage = (event) => {
      try {
        port.postMessage(JSON.parse(event.data));
      } catch (err) {
        server.send(JSON.stringify({ error: "Invalid JSON" }));
      }
    };
    
    // Worker -> Client
    port.onmessage = (event) => {
      server.send(JSON.stringify(event.data));
    };
    
    // Handle WebSocket close
    server.onclose = () => {
      port.close();
    };
    
    server.accept();
    
    return new Response(null, {
      status: 101,
      webSocket: client
    });
  }
}
```

```javascript {{ "title": "client.js" }}
// Connect to a chat room
const username = "user" + Math.floor(Math.random() * 1000);
const ws = new WebSocket("wss://chat.example.com/room123");

// Handle incoming messages
ws.onmessage = event => {
  const data = JSON.parse(event.data);
  
  if (data.type === "message") {
    console.log(`${data.message.username}: ${data.message.text}`);
  } else if (data.type === "history") {
    console.log("--- Message History ---");
    data.messages.forEach(msg => {
      console.log(`${msg.username}: ${msg.text}`);
    });
    console.log("---------------------");
  }
};

// Send a chat message
function sendMessage(text) {
  ws.send(JSON.stringify({
    text,
    username
  }));
}

// Example usage
ws.onopen = () => {
  sendMessage("Hello everyone!");
};
```
</CodeGroup>

### Redis-Like Server

This example demonstrates a minimalistic Redis-like key-value store server with get, set, and delete operations.

<CodeGroup>
```typescript {{ "title": "redisWorker.js" }}
export default {
  connect(event) {
    const port = event.ports[0];
    
    port.onmessage = async (event) => {
      const { command, key, value } = event.data;
      let response;
      
      // Handle different Redis-like commands - directly using storage API
      switch (command) {
        case "GET":
          const storedValue = await storage.get(`key:${key}`);
          response = { 
            status: "success", 
            value: storedValue !== undefined ? storedValue : null 
          };
          break;
          
        case "SET":
          await storage.put(`key:${key}`, value);
          response = { status: "success" };
          break;
          
        case "DEL":
          const exists = await storage.get(`key:${key}`) !== undefined;
          if (exists) {
            await storage.delete(`key:${key}`);
          }
          response = { 
            status: "success", 
            deleted: exists
          };
          break;
          
        case "KEYS":
          // For a real implementation, this would need support from the platform
          // to list keys with a specific prefix or pattern
          response = { 
            status: "error", 
            message: "KEYS command not supported in this simplified implementation" 
          };
          break;
          
        default:
          response = { 
            status: "error", 
            message: "Unknown command" 
          };
      }
      
      port.postMessage(response);
    };
  }
}
```

```typescript {{ "title": "server.js" }}
export default {
  async fetch(request) {
    const url = new URL(request.url);
    
    // Create or access the Redis worker
    // We use a single worker for the entire Redis server
    const worker = new ServerlessWorker("./redisWorker.js", "redis-server");
    const port = worker.port;
    
    // Handle POST requests for Redis commands
    if (request.method === "POST") {
      // Parse the command from the request body
      const command = await request.json();
      
      // Create a Promise that will be resolved with the response
      const { promise, resolve } = Promise.withResolvers();
      
      // Set up message handler
      port.onmessage = (event) => {
        resolve(new Response(JSON.stringify(event.data), {
          headers: { "Content-Type": "application/json" }
        }));
      };
      
      // Send the command to the worker
      port.postMessage(command);
      
      // Wait for the response
      return await promise;
    }
    
    return new Response("Invalid method", {
      status: 400,
      headers: { "Content-Type": "text/plain" }
    });
  }
}
```

```javascript {{ "title": "client.js" }}
// Redis client example

// Function to send a command to the Redis server
async function redisCommand(command, key, value = null) {
  const payload = { command, key };
  if (value !== null) {
    payload.value = value;
  }
  
  const response = await fetch("https://redis.example.com", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload)
  });
  
  return await response.json();
}

// Example usage
async function example() {
  // Set a value
  await redisCommand("SET", "greeting", "Hello, ServerlessWorker!");
  console.log("Set greeting key");
  
  // Get the value
  const result = await redisCommand("GET", "greeting");
  console.log("Get greeting:", result.value);
  
  // Delete the key
  const deleted = await redisCommand("DEL", "greeting");
  console.log("Deleted greeting:", deleted.deleted);
  
  // Verify it's gone
  const afterDelete = await redisCommand("GET", "greeting");
  console.log("After delete:", afterDelete.value);
}

example();
```
</CodeGroup>

## More Thought Required

### Data Migrations

WinterTC is great specification for stateless serverless since migrating providers is as simple as updating a DNS record.

However, stateful serverless is significantly different since **this spec is effectively defining a database**, since `ServerlessWorker`s have persistent storage. 

If the whole point of defining a common specification for stateful serverless is to encourage companies to adopt new technologies, then they'll also need assurances on their ability to migrate between providers. For example, Postgres provides the ability to use <a href="https://www.postgresql.org/docs/current/logical-replication.html" target="_blank">logical replication</a> for live migrations and is used frequently.

### Storage

The storage is the most rough section of this spec. It probably justifies its own spec.

### Connection Interruptions

`SharedWorker` connections cannot be interrupted. Can `ServerlessWorker` connections be interrupted? What happens if they are?

## Next Steps

We [uploaded this proposal to GitHub](https://github.com/rivet-dev/proposal-stateful-serverless-api) for feedback and iteration. Once there's more momentum, I hope to move this out of the `rivet-gg` organization in to an official W3C process.

### Feedback On GitHub

Please add feedback as a issue or pull request on the [proposal GitHub](https://github.com/rivet-dev/proposal-stateful-serverless-api).

### Reference Implementation

Once a general specification is implemented, a reference specification is trivial to implement on any of the providers previously mentioned. A compatibility layer could be implemented on the existing work with Rivet drivers.

## Survey: Help Shape the Specification

If you are either (a) a user of Durable Objects/Rivet actors or (b) want to aid in the direction, it'd be helpful to take the time to answer these questions as a [GitHub issue](https://github.com/rivet-dev/proposal-stateful-serverless-api/issues) on the proposal:

- **Do we attempt to specify existing APIs or implement based on existing work by W3C?** This spec aims to build on patterns already defined by W3C, however it might make sense to stick to better documenting overlap between existing providers (currently only Durable Objects & Rivet as far as I know). My understanding is WinterTC attempted to create a spec that matched existing APIs. I'd be happy to rewrite this spec based on the `fetch` API if the answer to this is _yes_.
- **What are the most common hesitations for companies adopting stateful serverless?** Is there something this spec can address?
- **What are the most common problems with stateful serverless that contributes to cognitive load?** Does the simplicity of this spec help address that?
- **What reference design patterns should we be designing for?** The examples attached attempt to provide a very basic overview of common patterns on Durable Objects, but does not cover use cases like worker-worker communication.
- **How should storage be represented?** This spec proposes using a simple KV. This storage mechanism might be too restrictive, or we might not want to write a spec for storage altogether and keep `ServerlessWorker` very simple. It may make sense to leverage an existing storage spec or write a new separate storage spec that is combined with this.
- **Should this be broader than web technologies?** This spec makes the assumption it's part of W3C and leverages web technologies. However, there are a few container platforms that do not use web technologies which might fall under this scope.
- **Should this spec care about regions?** `ServerlessWorker` shine when they run at the edge. Should they have a default for configuring locations, similar to Rivet and Cloudflare?

_Thanks to [LambrosPetrou](https://x.com/LambrosPetrou), [firtozb](https://x.com/firtozb), [uripont_](https://x.com/uripont_), and [p\_mbanugo](https://x.com/p_mbanugo) for feedback on the initial document._
