Terse vs Rivet Actors: persistence, APIs, and deployment

Terse Durable Actors is a Rivet Actors alternative for teams that prefer actor classes, explicit persisted fields, and serialized calls by default. Rivet offers a state-and-actions API and multiple deployment models, including managed hosting and self-hosting. The decision should turn on application semantics and operating requirements.

We build Terse. This comparison uses documentation available on October 17, 2026. It is an architectural evaluation, not a benchmark of either runtime.

Both address stateful applications

An actor owns state behind an identity. For a chat application, that identity might be a conversation; for a collaborative editor, a document. Instead of having every caller manipulate shared records directly, the application sends work to the actor that owns them.

Rivet Actors support agent orchestration, realtime applications, and per-tenant data. Terse Durable Actors target realtime applications and agent coordination with TypeScript and Python SDKs.

Both have WebSocket support, SQLite interfaces, and self-hosting options. Those capabilities establish a useful shortlist. They do not settle which system fits your application.

Terse vs Rivet at a glance

Decision Terse Durable Actors Rivet Actors
TypeScript API Classes extending Actor RivetKit actor definitions with state and actions
Simple durable state Decorated persisted fields Automatically saved actor state
Save boundary Successful calls under the default model State saves are throttled; immediate saves can be awaited
Queryable actor data Per-actor SQLite Per-actor SQLite
Typed client Generated actor stubs Type inference from the registry
Deployment Documented self-hosting on GCP Managed, own-worker, BYOC, and self-host options

The save-boundary row is worth investigating before comparing syntax.

When does a state change become durable?

Consider an agent session that records an approved action and then calls an external service. If the process stops between those steps, what will the next session see?

Terse's TypeScript guide describes persisted fields and SQLite changes committing together when a method succeeds. Calls are serialized across awaits by default, and failed calls roll back local changes under that model.

Rivet's state documentation describes automatic saves on a throttle, with a default interval of one second. Saves are not tied to action boundaries. An awaited c.saveState({ immediate: true }) waits for a write to complete; a plain c.saveState() schedules one without waiting.

These contracts suggest different application designs. With method-boundary commits, choose methods that represent coherent local state transitions. With throttled state saves, identify the points where the application must wait for persistence before proceeding.

Neither approach turns an external API call into part of a local transaction. For a payment or email, use an idempotency key and define how the application reconciles an ambiguous response. Test the case where the provider succeeds but the actor stops before recording the result.

Classes or state and actions?

Here is a minimal session actor in Terse:

import { Actor, Persisted } from "durable-actors"

export class Session extends Actor {
  @Persisted private title = "Untitled"

  async rename(title: string): Promise<string> {
    this.title = title
    return this.title
  }
}

In RivetKit, a comparable definition uses state and actions:

import { actor } from "rivetkit"

export const session = actor({
  state: { title: "Untitled" },
  actions: {
    rename: (c, title: string) => {
      c.state.title = title
      return c.state.title
    },
  },
})

These are actor definitions, not complete server applications. Terse's guide covers client generation; Rivet's actions reference covers registering definitions and typed client calls. Similar return values in these examples do not imply identical durability guarantees.

Choose the style that makes ownership and state changes easiest for your team to review. Put authorization and runtime input validation around callable operations regardless of TypeScript types.

Test concurrency with a slow operation

A chat session needs to stay useful while a model request runs. Build that scenario into your evaluation: start generation, request status from another client, and attempt a second mutation.

Terse's default serialization keeps another call from proceeding until the current call finishes, including its awaits. @Interleave permits progress while the decorated method waits, but disables automatic rollback for all calls in that class. See the interleaving reference.

For Rivet, implement the same scenario using its actions and observe status updates, cancellation, and state persistence under failure. Do not assume the two runtimes have equivalent concurrency behavior because both use the word actor.

Write down the invariant before testing: for example, a session can have only one accepted generation request at a time. A responsive UI is useful only if that invariant still holds.

SQLite and realtime features

Both projects support actor-owned SQL data. Terse exposes this.db; Rivet exposes SQLite through c.db, including migration and query APIs. Porting between them still requires reviewing transaction boundaries, schema setup, and supported statements.

Rivet also documents realtime events, queues, and scheduling in its actor overview. Terse's socket guide covers connection hooks, tags, broadcasts, and connection grants.

For a multiplayer evaluation, reconnect after a runtime restart and check what the client receives. Decide how clients recover updates missed while disconnected and how access is revoked. A successful broadcast on a healthy connection is only the first test.

Self-hosting is an option on both sides

Terse's documented production deployment uses GKE Sandbox, PostgreSQL, GCS, and a Helm chart. Google Cloud is the supported hosting platform in that guide. Your team operates it.

Rivet's deployment documentation distinguishes managed hosting, your own workers connected to Rivet Cloud, bring your own cloud, and full self-hosting. Running your own workers and operating the whole control plane are different responsibilities.

Evaluate the complete deployment your team would use. Include the storage layer, upgrades, backups, capacity planning, and recovery work. We have not run a matched cost or latency benchmark, so this comparison makes no cheaper-or-faster claim.

How to evaluate a migration

If you are looking for a Rivet Actors alternative, use one representative session rather than rewriting the whole application first.

  1. Keep the same session identity and define which data must survive a restart.
  2. Map Rivet state to Terse persisted fields, and review SQL data separately.
  3. Replace action calls and client setup with generated actor clients.
  4. Test failure before saving, after saving, and during an external call.
  5. Reconnect clients and verify authorization, state recovery, and duplicate-request handling.

Plan state export, import, and ownership at cutover. Similar actor concepts do not provide automatic data migration or API compatibility.

Which should you choose?

Choose Terse Durable Actors when its class-based model, explicit persisted fields, and default serialization suit your code, and you want to operate its documented runtime.

Choose Rivet Actors when its state-and-actions model or deployment options fit your team better. If managed hosting is a requirement, include that in the decision early.

Also considering Cloudflare? Read Terse vs Cloudflare Durable Objects for the managed-platform comparison.

Try Terse with one session

The Durable Actors quickstart starts a local project:

bunx durable-actors init my-actors
cd my-actors
bun install
bunx durable-actors dev

Add a persisted field, generate a client with the TypeScript guide, and run your restart and concurrency checks. That gives your team a concrete basis for choosing.

Build your first Durable Actor →