Skip to main content
Sign In
Features

State & Storage

Choose where to store data in your actors: in-memory state for small serializable values, embedded SQLite for large or queryable data, and ephemeral variables for connections to external databases and non-serializable runtime objects.

Actors give you several places to store data. Choosing the right one keeps your actor fast, durable, and easy to reason about.

Choosing Where to Store Data

NeedUse
Small, simple, serializable values (counters, flags, a small map)c.state
Large / relational / queryable / durable dataSQLite (c.db) — see SQLite docs
Data in an external database, or non-serializable runtime objects (connections, clients, emitters)createVars / c.vars

In-memory state (c.state) is the simplest option and the right default for small amounts of data. As soon as your data grows large, becomes relational, or needs to be queried, reach for SQLite instead. Use ephemeral variables (c.vars) for runtime-only objects like database clients or for loading data from an external database.

In-Memory State

Actor state provides the best of both worlds: it’s stored in-memory and persisted automatically. This lets you work with the data without added latency while still surviving crashes and upgrades.

In-memory state is meant for small, simple values such as counters, flags, or a small map. When your data grows large or needs querying, use SQLite instead.

Initializing State

There are two ways to define an actor’s initial state:

The createState function is called once when the actor is first created. See Lifecycle for more details.

Modifying State

To update state, modify the state property on the context object (c.state) in your actions:

import { actor } from "rivetkit";

const counter = actor({
  state: { count: 0 },

  actions: {
    // Define action to update state
    increment: (c) => {
      // Update state, this will automatically be persisted
      c.state.count += 1;
      return c.state.count;
    },

    add: (c, value: number) => {
      c.state.count += value;
      return c.state.count;
    }
  }
});

Only state stored in the state object will be persisted. Any other variables or properties outside of this are not persisted.

State Saves

Actors automatically handle persisting state transparently. This happens at the end of every action if the state has changed. State is also automatically saved after onFetch and onWebSocket handlers finish executing.

For onWebSocket handlers specifically, you’ll need to manually save state using c.saveState() while the WebSocket connection is open if you want state changes to be persisted immediately. This is because WebSocket connections can remain open for extended periods, and state changes made during event handlers (like message events) won’t be automatically saved until the connection closes.

In other cases where you need to force a state change mid-action, you can use c.saveState(). This should only be used if your action makes an important state change that needs to be persisted before the action completes.

Immediate vs Throttled Saves

c.saveState() supports two modes:

  • c.saveState({ immediate: true }) saves state to storage right away. await resolves once the write completes. Use this when you need to guarantee persistence before continuing (e.g. before a risky async operation).
  • c.saveState() (without immediate: true) schedules a throttled save. await will not resolve until the next flush cycle, which can take up to stateSaveInterval (default: 10 seconds). This batches rapid state changes to reduce write frequency, but means the caller blocks until the flush fires.

If you want to save state promptly during a WebSocket message handler, use immediate: true.

import { actor } from "rivetkit";

// Mock risky operation
async function someRiskyOperation() {
  await new Promise(resolve => setTimeout(resolve, 1000));
}

const criticalProcess = actor({
  state: {
    steps: [] as string[],
    currentStep: 0
  },

  actions: {
    processStep: async (c) => {
      // Update to current step
      c.state.currentStep += 1;
      c.state.steps.push(`Started step ${c.state.currentStep}`);

      // Force save state before the async operation
      await c.saveState({ immediate: true });

      // Long-running operation that might fail
      await someRiskyOperation();

      // Update state again
      c.state.steps.push(`Completed step ${c.state.currentStep}`);

      return c.state.currentStep;
    }
  }
});

State Isolation

Each actor’s state is completely isolated, meaning it cannot be accessed directly by other actors or clients.

To interact with an actor’s state, you must use Actions. Actions provide a controlled way to read from and write to the state.

If you need a shared state between multiple actors, see sharing and joining state.

Type Limitations

State is currently constrained to the following types:

  • null
  • undefined
  • boolean
  • string
  • number
  • BigInt
  • Date
  • RegExp
  • Error
  • Typed arrays (Uint8Array, Int8Array, Float32Array, etc.)
  • Map
  • Set
  • Array
  • Plain objects

SQLite

For data that is large, relational, queryable, or larger than memory, use the embedded SQLite database available on c.db.

Each actor instance has its own SQLite database, scoped to that actor. Because Rivet Actors keep compute and storage together, queries avoid network round trips to an external database. SQLite stores data on disk, so you can work with datasets that do not fit in actor memory, and you get a full relational engine with tables, indexes, JOINs, constraints, and transactions.

For complete documentation, see:

  • SQLite — raw SQL queries against the embedded per-actor database.
  • SQLite + Drizzle — typed schema and query APIs with the Drizzle ORM.

Ephemeral Variables

In addition to persisted state, actors can store ephemeral data that is not saved to permanent storage using vars. This is useful for temporary data that only needs to exist while the actor is running, non-serializable objects like database connections or event emitters, and loading initial data from an external database.

vars is designed to complement state, not replace it. Most actors that need it will use both: state for critical business data and vars for ephemeral or non-serializable data.

Initializing Variables

There are two ways to define an actor’s initial vars:

Using Variables

Vars can be accessed and modified through the context object with c.vars:

import { actor } from "rivetkit";

// Mock event emitter for demonstration
interface EventEmitter {
  on: (event: string, callback: (data: number) => void) => void;
  emit: (event: string, data: number) => void;
}

function createEventEmitter(): EventEmitter {
  const listeners: Record<string, ((data: number) => void)[]> = {};
  return {
    on: (event, callback) => {
      listeners[event] = listeners[event] || [];
      listeners[event].push(callback);
    },
    emit: (event, data) => {
      listeners[event]?.forEach(cb => cb(data));
    }
  };
}

const counter = actor({
  // Persistent state - saved to storage
  state: { count: 0 },

  // Create ephemeral objects that won't be serialized
  createVars: () => {
    // Create an event emitter (can't be serialized)
    const emitter = createEventEmitter();

    // Set up event listener directly in createVars
    emitter.on('count-changed', (newCount) => {
      console.log(`Count changed to: ${newCount}`);
    });

    return { emitter };
  },

  actions: {
    increment: (c) => {
      // Update persistent state
      c.state.count += 1;

      // Use non-serializable emitter
      c.vars.emitter.emit('count-changed', c.state.count);

      return c.state.count;
    }
  }
});

Connecting to External Databases

Because createVars runs on every actor start, it’s the natural place to open a connection to an external database such as Postgres or Redis and load any data your actor needs. The connection lives only in memory and is never serialized:

import { actor } from "rivetkit";
import { Pool } from "pg";

const userActor = actor({
  state: { profile: null as Record<string, unknown> | null },

  // Open a connection to the external database and load initial data on every start
  createVars: async (c) => {
    const pool = new Pool({ connectionString: process.env.DATABASE_URL });
    const result = await pool.query("SELECT * FROM users WHERE id = $1", [c.key[0]]);
    return { pool, profile: result.rows[0] };
  },

  actions: {
    updateEmail: async (c, email: string) => {
      await c.vars.pool.query("UPDATE users SET email = $1 WHERE id = $2", [email, c.key[0]]);
    }
  }
});

Use this pattern when your source of truth lives in an external database. For data owned entirely by the actor, prefer in-memory state or SQLite, which require no external infrastructure.

When to Use vars vs state

In practice, most actors that need both will use them together: state for critical business data and vars for ephemeral or non-serializable data.

Use vars when:

  • You need to store temporary data that doesn’t need to survive restarts.
  • You need to maintain runtime-only references that can’t be serialized (database connections, event emitters, class instances, etc.).
  • You need to load data from or write through to an external database.

Use state when:

  • The data must be preserved across actor sleeps, restarts, updates, or crashes.
  • The information is essential to the actor’s core functionality and business logic.

Debugging

  • GET /inspector/state returns the actor’s current persisted state and isStateEnabled.
  • PATCH /inspector/state lets you set state directly while debugging.
  • In non-dev mode, inspector endpoints require authorization.

API Reference