Skip to main content
Lifecycle & Config

Lifecycle

Understand Rivet Actor creation, wake, sleep, connection, migration, and destruction hooks across an instance's lifecycle.

Actors follow a well-defined lifecycle with hooks at each stage. Understanding these hooks is essential for proper initialization, data management, and cleanup.

Lifecycle

Actors transition through several states during their lifetime. Each transition triggers specific hooks that let you initialize resources, manage connections, and clean up state.

Loading ──Start──▶ Ready ──spawn driver──▶ Started
                                              │
     ┌────────────────────────────────────────┤
     │                                        │
     │ idle timer + can_sleep                 │ Destroy command
     ▼                                        ▼
  SleepGrace ─── grace window closes ──▶ Destroying
     │                                        │
     ▼                                        │
  SleepFinalize ──── stop sequence ───────────┤
                                              ▼
                                          Terminated

On Create (runs once per actor)

  1. onMigrate
  2. onCreate
  3. createVars
  4. onWake, which starts any background tasks
  5. run workflow (background, does not block)

On Destroy

  1. c.abortSignal fires
  2. onDestroy

On Wake (after sleep, restart, or crash)

  1. onMigrate
  2. createVars
  3. onWake, which starts any background tasks
  4. run workflow (background, does not block)

On Sleep (after idle period)

  1. c.abortSignal fires, so the run workflow and background tasks can stop
  2. onSleep, while the run workflow finishes (with timeout)

On Connect (per client)

  1. onBeforeConnect
  2. onConnect

On Disconnect (per client)

  1. onDisconnect

On Inbound Action Invoke (per action call)

  1. Action handler executes

On Event Subscription Request (per subscribe request)

  1. events.<name>.canSubscribe (if defined)
  2. Subscription is applied

Lifecycle Hooks

Actor lifecycle hooks are defined as functions in the actor configuration.

onMigrate

The onMigrate hook runs on every actor start, before onCreate, createVars, and onWake. Can be async. It runs early so that database migrations are applied before any other lifecycle hook accesses the database. The second parameter is true when the actor is being created for the first time.

import { actor } from "rivetkit";

const counter = actor({
  onMigrate: (c, isNew) => {
    // Run database migrations before any other lifecycle hook
  },

  actions: { /* ... */ }
});

vars

The vars constant defines ephemeral variables for the actor. These variables are not persisted and are useful for storing runtime-only data. The value for vars must be clonable via structuredClone. See ephemeral variables documentation for more information.

import { actor } from "rivetkit";

const counter = actor({
  vars: { lastAccessTime: 0 },
  actions: { /* ... */ }
});

createVars

The createVars function dynamically initializes ephemeral variables. Can be async. Use this when you need to initialize values at runtime. See ephemeral variables documentation for more information.

import { actor } from "rivetkit";

interface CounterVars {
  lastAccessTime: number;
  emitter: EventTarget;
}

const counter = actor({
  createVars: (c): CounterVars => ({
    lastAccessTime: Date.now(),
    emitter: new EventTarget()
  }),
  actions: { /* ... */ }
});

onCreate

The onCreate hook is called when the actor is first created and receives the actor’s input. Can be async. It runs only once per actor, so use it for one-time setup such as seeding SQLite from input.

import { actor } from "rivetkit";
import { db } from "rivetkit/db";

const counter = actor({
  db: db({
    onMigrate: async (db) => {
      await db.execute(`
        CREATE TABLE IF NOT EXISTS counter (
          id INTEGER PRIMARY KEY CHECK (id = 1),
          count INTEGER NOT NULL
        );
      `);
    },
  }),

  onCreate: async (c, input: { initialCount: number }) => {
    // Seed the database from input. This runs only once per actor.
    await c.db.execute(
      "INSERT INTO counter (id, count) VALUES (1, ?)",
      input.initialCount,
    );
    console.log("Actor created with initial count:", input.initialCount);
  },

  actions: { /* ... */ }
});

onDestroy

The onDestroy hook is called when the actor is being permanently destroyed. Can be async. Use this for final cleanup operations like closing external connections, releasing resources, or writing any final data to the database.

The actor is still fully functional when onDestroy runs. You can access the database, broadcast events, call waitUntil, and use schedule.after. Database writes made during onDestroy complete before the actor is torn down.

import { actor } from "rivetkit";

const gameSession = actor({
  onDestroy: (c) => {
    // Clean up any external resources
  },
  actions: { /* ... */ }
});

onWake

This hook is called any time the actor is started (e.g. after restarting, upgrading code, or crashing). Can be async.

This is called after the actor has been initialized but before any connections are accepted.

Use this hook to set up any resources or start any background tasks, such as setInterval. See Background tasks for long-running loops.

import { actor } from "rivetkit";
import { db } from "rivetkit/db";

const counter = actor({
  db: db({
    onMigrate: async (db) => {
      await db.execute(`
        CREATE TABLE IF NOT EXISTS counter (
          id INTEGER PRIMARY KEY CHECK (id = 1),
          count INTEGER NOT NULL
        );
      `);
      await db.execute("INSERT OR IGNORE INTO counter (id, count) VALUES (1, 0)");
    },
  }),
  vars: { intervalId: null as NodeJS.Timeout | null },

  onWake: async (c) => {
    const rows = await c.db.execute<{ count: number }>(
      "SELECT count FROM counter WHERE id = 1",
    );
    console.log('Actor started with count:', rows[0].count);

    // Set up interval for automatic counting
    const intervalId = setInterval(async () => {
      const updated = await c.db.execute<{ count: number }>(
        "UPDATE counter SET count = count + 1 WHERE id = 1 RETURNING count",
      );
      c.broadcast("countChanged", updated[0].count);
      console.log('Auto-increment:', updated[0].count);
    }, 10000);

    // Store interval ID in vars to clean up later if needed
    c.vars.intervalId = intervalId;
  },

  actions: {
    stop: (c) => {
      if (c.vars.intervalId) {
        clearInterval(c.vars.intervalId);
        c.vars.intervalId = null;
      }
    }
  }
});

onSleep

This hook is called when the actor is going to sleep. Can be async. Use this to clean up resources, close connections, or perform any shutdown operations.

The actor is still fully functional when onSleep runs. You can access the database, broadcast events, call waitUntil, and use schedule.after. Database writes made during onSleep complete before the actor finishes sleeping.

This hook may not always be called in situations like crashes or forced terminations. Don’t rely on it for critical cleanup operations.

import { actor } from "rivetkit";

const counter = actor({
  vars: {
    count: 0,
    intervalId: null as NodeJS.Timeout | null,
  },

  onWake: (c) => {
    // Set up interval when actor wakes
    c.vars.intervalId = setInterval(() => {
      c.vars.count++;
      console.log('Auto-increment:', c.vars.count);
    }, 10000);
  },

  onSleep: (c) => {
    console.log('Actor going to sleep, cleaning up...');

    // Clean up interval before sleeping
    if (c.vars.intervalId) {
      clearInterval(c.vars.intervalId);
      c.vars.intervalId = null;
    }

    // Perform any other cleanup
    console.log('Final count:', c.vars.count);
  },

  actions: { /* ... */ }
});

run

Set run to a workflow to give the actor a durable entry point. The workflow starts after onWake and runs in the background without blocking startup. Each completed step is recorded, so after a sleep, crash, or upgrade the workflow resumes where it left off instead of starting over.

A workflow that returns leaves the actor alive, and the actor sleeps once it is idle. To clean up a one-shot job when it is done, destroy the actor from the last step:

import { actor } from "rivetkit";
import { workflow } from "rivetkit/workflow";

// Example: Finite job that destroys the actor when done
const oneShotJob = actor({
  run: workflow(async (ctx) => {
    // A completed step is recorded, so a restart after the job finishes does not run it again
    await ctx.step("process-job", async () => {
      await processJob();
    });

    await ctx.step("destroy", async (step) => {
      step.destroy();
    });
  }),
});

async function processJob(): Promise<void> {}

Background tasks

For long-running work that does not need to be durable, such as a tick loop, start a task from onWake:

  1. onWake starts the task and stores its promise in c.vars. onWake runs on every start, so the task restarts after a sleep, crash, or upgrade.
  2. The task checks c.aborted and passes c.abortSignal to anything it waits on, so it exits when the actor starts shutting down.
  3. onSleep waits for the task to exit. c.abortSignal fires before onSleep runs, so the task is already stopping.
import { type ActorContextOf, actor } from "rivetkit";

// Example: Tick loop
const tickActor = actor({
  vars: {
    tickCount: 0,
    // `onWake` stores its loop here so `onSleep` can wait for it.
    loop: null as Promise<void> | null,
  },

  // `onWake` runs on every start, so the loop restarts after a sleep or crash
  onWake: (c) => {
    c.vars.loop = runTickLoop(c).catch((error) => {
      c.log.error({ msg: "tick loop failed", error });
    });
  },

  // `c.abortSignal` fires before `onSleep`, so the loop is already exiting
  onSleep: async (c) => {
    await c.vars.loop;
  },

  actions: {
    getTickCount: (c) => c.vars.tickCount
  }
});

async function runTickLoop(c: ActorContextOf<typeof tickActor>): Promise<void> {
  c.log.info("Background loop started");

  while (!c.aborted) {
    c.vars.tickCount++;
    c.log.info({ msg: "tick", count: c.vars.tickCount });

    await delay(1_000, c.abortSignal);
  }

  c.log.info("Background loop exiting gracefully");
}

// Resolve after `ms`, or as soon as the actor starts shutting down
function delay(ms: number, signal: AbortSignal): Promise<void> {
  return new Promise((resolve) => {
    if (signal.aborted) {
      resolve();
      return;
    }

    const finish = () => {
      clearTimeout(timer);
      signal.removeEventListener("abort", finish);
      resolve();
    };
    const timer = setTimeout(finish, ms);
    signal.addEventListener("abort", finish, { once: true });
  });
}

A background task does not keep the actor awake. Once the actor has no connections or requests, it sleeps after sleepTimeout, the task exits, and onWake starts it again on the next wake. Wrap work that must finish in c.keepAwake(promise). Wrapping the whole loop keeps the actor awake until it is destroyed, so only do that for an actor that must never sleep.

onBeforeConnect

The onBeforeConnect hook is called whenever a new client connects to the actor. Can be async. Clients can pass parameters when connecting, accessible via params. This hook is used for connection validation and can throw errors to reject connections.

The onBeforeConnect hook is used solely for validation. To keep per-connection data, store it in c.vars keyed by conn.id in onConnect and remove it in onDisconnect.

import { actor } from "rivetkit";

function validateToken(token: string): boolean {
  return token.length > 0;
}

type ConnParams = {
  userId?: string;
  role?: string;
  authToken?: string;
};

interface ConnInfo {
  userId: string;
  role: string;
  joinTime: number;
}

const chatRoom = actor({
  // Per-connection data lives in memory, keyed by connection ID
  createVars: () => ({ connections: new Map<string, ConnInfo>() }),

  // Validate connections before accepting them
  onBeforeConnect: (_c, params: ConnParams) => {
    // Validate authentication
    const authToken = params.authToken;
    if (!authToken || !validateToken(authToken)) {
      throw new Error("Invalid authentication");
    }

    // Authentication is valid, so the connection will proceed to onConnect
  },

  // Record per-connection data once the connection is accepted
  onConnect: (c, conn) => {
    const params = conn.params;
    c.vars.connections.set(conn.id, {
      userId: params.userId || "anonymous",
      role: params.role || "guest",
      joinTime: Date.now()
    });
  },

  onDisconnect: (c, conn) => {
    c.vars.connections.delete(conn.id);
  },

  actions: { /* ... */ }
});

Connections cannot interact with the actor until this method completes successfully. Throwing an error will abort the connection. This can be used for authentication - see Permissions for details.

onConnect

Executed after the client has successfully connected. Can be async. Receives the connection object as a second parameter, whose conn.params holds the parameters the client passed.

import { actor } from "rivetkit";

const chatRoom = actor({
  createVars: () => ({
    users: {} as Record<string, { online: boolean; lastSeen: number }>,
    userIds: new Map<string, string>(),
  }),

  onConnect: (c, conn) => {
    // Remember which user owns this connection
    const params = conn.params as { userId?: string } | undefined;
    const userId = params?.userId ?? "anonymous";
    c.vars.userIds.set(conn.id, userId);

    // Add user to the room's user list
    c.vars.users[userId] = {
      online: true,
      lastSeen: Date.now()
    };

    // Broadcast that a user joined
    c.broadcast("userJoined", { userId, timestamp: Date.now() });

    console.log(`User ${userId} connected`);
  },

  actions: { /* ... */ }
});

Messages will not be processed for this actor until this hook succeeds. Errors thrown from this hook will cause the client to disconnect.

canSubscribe

Use the schema-level events.<name>.canSubscribe hook to authorize event subscriptions. It runs before each inbound subscription request, can be async, and must return a boolean.

For actions, enforce authorization directly inside each action handler.

import { actor, event, UserError } from "rivetkit";

type Role = "member" | "admin";

const securedActor = actor({
  // Each connection's role is tracked in memory, keyed by connection ID
  createVars: () => ({ roles: new Map<string, Role>() }),

  onConnect: (c, conn) => {
    const params = conn.params as { role?: Role } | undefined;
    c.vars.roles.set(conn.id, params?.role ?? "member");
  },

  onDisconnect: (c, conn) => {
    c.vars.roles.delete(conn.id);
  },

  events: {
    publicFeed: event<{ text: string }>(),
    adminFeed: event<{ text: string }>({
      canSubscribe: (c) => c.conn !== undefined && c.vars.roles.get(c.conn.id) === "admin",
    }),
  },

  actions: {
    publicAction: () => "ok",
    privateAction: (c) => {
      if (c.vars.roles.get(c.conn.id) !== "admin") {
        throw new UserError("Forbidden", { code: "forbidden" });
      }
      return "secret";
    },
  },
});

Use deny-by-default rules and return false unless explicitly allowed. See Permissions for full guidance.

onDisconnect

Called when a client disconnects from the actor. Can be async. Receives the connection object as a second parameter. Use this to clean up any connection-specific resources.

import { actor } from "rivetkit";

const chatRoom = actor({
  createVars: () => ({
    users: {} as Record<string, { online: boolean; lastSeen: number }>,
    userIds: new Map<string, string>(),
  }),

  onConnect: (c, conn) => {
    const params = conn.params as { userId?: string } | undefined;
    c.vars.userIds.set(conn.id, params?.userId ?? "anonymous");
  },

  onDisconnect: (c, conn) => {
    // Update user status when they disconnect
    const userId = c.vars.userIds.get(conn.id) ?? "anonymous";
    c.vars.userIds.delete(conn.id);
    if (c.vars.users[userId]) {
      c.vars.users[userId].online = false;
      c.vars.users[userId].lastSeen = Date.now();
    }

    // Broadcast that a user left
    c.broadcast("userLeft", { userId, timestamp: Date.now() });

    console.log(`User ${userId} disconnected`);
  },

  actions: { /* ... */ }
});

onRequest

The onRequest hook handles HTTP requests sent to your actor at /gateway/{actorId}/request/*. Can be async. It receives the request context and a standard Request object, and should return a Response object.

See Request Handler for more details.

import { actor } from "rivetkit";

const apiActor = actor({
  vars: { requestCount: 0 },

  onRequest: (c, request) => {
    const url = new URL(request.url);
    c.vars.requestCount++;

    if (url.pathname === "/api/status") {
      return new Response(JSON.stringify({
        status: "ok",
        requestCount: c.vars.requestCount
      }), {
        headers: { "Content-Type": "application/json" }
      });
    }

    return new Response("Not found", { status: 404 });
  },

  actions: { /* ... */ }
});

onWebSocket

The onWebSocket hook handles WebSocket connections to your actor. Can be async. It receives the actor context and a WebSocket object. Use this to set up WebSocket event listeners and handle real-time communication.

See WebSocket Handler for more details.

import { actor } from "rivetkit";

const realtimeActor = actor({
  vars: { connectionCount: 0 },

  onWebSocket: (c, websocket) => {
    c.vars.connectionCount++;

    // Send welcome message
    websocket.send(JSON.stringify({
      type: "welcome",
      connectionCount: c.vars.connectionCount
    }));

    // Handle incoming messages
    websocket.addEventListener("message", (event) => {
      const data = JSON.parse(event.data);

      if (data.type === "ping") {
        websocket.send(JSON.stringify({
          type: "pong",
          timestamp: Date.now()
        }));
      }
    });

    // Handle connection close
    websocket.addEventListener("close", () => {
      c.vars.connectionCount--;
    });
  },

  actions: { /* ... */ }
});

onBeforeActionResponse

The onBeforeActionResponse hook is called before sending an action response to the client. Can be async. Use this hook to modify or transform the output of an action before it’s sent to the client. This is useful for formatting responses, adding metadata, or applying transformations to the output.

import { actor } from "rivetkit";

const loggingActor = actor({
  vars: { requestCount: 0 },

  onBeforeActionResponse: (c, actionName, args, output) => {
    // Log action calls
    console.log(`Action ${actionName} called with args:`, args);
    console.log(`Action ${actionName} returned:`, output);

    c.vars.requestCount++;
    c.broadcast("actionResponseLogged", {
      actionName,
      timestamp: Date.now(),
      requestCount: c.vars.requestCount,
    });

    return output;
  },

  actions: {
    getUserData: (c, userId: string) => {
      c.vars.requestCount++;

      // This response is returned after onBeforeActionResponse runs
      return {
        userId,
        profile: { name: "John Doe", email: "john@example.com" },
        lastActive: Date.now()
      };
    },

    getStats: (c) => {
      // This also passes through onBeforeActionResponse
      return {
        requestCount: c.vars.requestCount,
        uptime: process.uptime()
      };
    }
  }
});

Sleeping

Actors automatically sleep after a period of inactivity to free up resources. When a request arrives for a sleeping actor, it wakes up, restores its state, and handles the request.

When Actors Sleep

Idle Timeout

An actor is considered idle and eligible to sleep when all of the following are true:

  • No active HTTP requests
  • No active connections (unless they are hibernatable WebSockets)
  • No active run workflow
  • No outstanding c.keepAwake(promise) promises
  • No pending disconnect callbacks
  • No async onWebSocket event handlers (eg open, message, close) still running

Background tasks started from onWake do not count as activity, so the actor can sleep while they run. Wrap work that must finish in c.keepAwake(promise).

Once the actor becomes idle, the sleep timer starts. After sleepTimeout (default 30 seconds) of continuous inactivity, the actor begins the sleep process. Any activity resets the timer.

Outbound requests (e.g. fetch calls) do not count as activity and will not keep the actor awake. Wrap them with c.waitUntil() if they must complete before the actor sleeps.

Upgrades & Eviction

The platform may move an actor to another node, for example when its node stops during a deploy. The same shutdown sequence runs, then the actor is rescheduled on another node and wakes up with its persisted state.

Use onSleep, waitUntil, or keepAwake to control the length of the grace period before the actor moves to another node.

Manual Lifecycle Controls

You can also trigger lifecycle transitions from the control plane API. Set $RIVET_API to your control plane URL, such as https://rivet.example.com. These endpoints are useful for operational workflows, debugging, and forcing an actor to move through the same sleep or reschedule path that the platform would normally trigger.

curl -X POST \
  "$RIVET_API/actors/$ACTOR_ID/sleep?namespace=$NAMESPACE" \
  -H "Authorization: Bearer $RIVET_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
curl -X POST \
  "$RIVET_API/actors/$ACTOR_ID/reschedule?namespace=$NAMESPACE" \
  -H "Authorization: Bearer $RIVET_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'

/sleep asks the actor to enter the normal sleep shutdown sequence. /reschedule asks the platform to allocate the actor again, which is useful after crashes or when you need to force a fresh placement. Both endpoints require the actor ID and namespace.

Skip Ready Wait

The gateway normally holds requests until the actor is ready. The actor is not ready during startup (before onWake finishes) or during the sleep grace period (while onSleep and waitUntil are running). Probes and readiness checks can opt out with skipReadyWait to reach the actor’s onRequest or onWebSocket handler in either window.

See Skip Ready Wait on the JavaScript client page for usage.

Keeping the Actor Awake

RivetKit gives you two primitives for holding the actor awake across background work. Both take a Promise and differ in how they interact with idle sleep and the grace period.

MethodAcceptsBlocks idle sleepBlocks grace finalizeUse case
c.keepAwake(promise)Promise<T> (returns same promise)YesYesCritical work that must keep the actor running end to end (for example a turn in a game, an ongoing tool call).
c.waitUntil(promise)Promise<unknown> (returns void)NoYesBest-effort finalization work that may complete during the grace window (for example analytics flushes, cleanup writes).

c.keepAwake(promise) is the preferred primitive for long-running work the actor should not sleep through. It holds a keep-awake counter until the promise settles, which blocks both idle sleep and the grace finalize step. The promise is returned unchanged, so you can await it if you need the value.

import { actor } from "rivetkit";

const sessionActor = actor({
  vars: {
    activeTurns: 0,
  },

  actions: {
    runTurn: async (c, input: string) => {
      c.vars.activeTurns += 1;
      try {
        const result = await c.keepAwake(processTurn(input));
        return result;
      } finally {
        c.vars.activeTurns -= 1;
      }
    },
  }
});

declare function processTurn(input: string): Promise<string>;

On Sleep Hook

The onSleep hook runs during shutdown for cleanup like clearing intervals or closing connections. It is best-effort and will not run if the actor crashes.

import { actor } from "rivetkit";

const myActor = actor({
  vars: {
    count: 0,
    intervalId: null as ReturnType<typeof setInterval> | null,
  },

  onWake: (c) => {
    c.vars.intervalId = setInterval(() => { c.vars.count++; }, 10_000);
  },

  onSleep: (c) => {
    if (c.vars.intervalId) clearInterval(c.vars.intervalId);
  },

  actions: { /* ... */ }
});

Wait Before Sleep

c.waitUntil(promise) registers a background promise that must resolve before the actor finishes sleeping. Use this to flush data or finish in-flight work during shutdown without blocking the main execution flow.

import { actor } from "rivetkit";

const analyticsActor = actor({
  vars: { events: [] as string[] },

  actions: {
    track: (c, event: string) => {
      c.vars.events.push(event);

      // The actor will wait for this to complete before sleeping.
      c.waitUntil(
        fetch("https://analytics.example.com/ingest", {
          method: "POST",
          body: JSON.stringify({ event }),
        }).then(() => {})
      );
    },
  },
});

The actor waits up to sleepGracePeriod for graceful sleep work during the shutdown sequence. That single budget covers onSleep, waitUntil, keepAwake, async raw WebSocket handlers such as message and close. By default, this graceful sleep window is 15 seconds total. If the timeout is exceeded, the actor proceeds with sleep anyway.

Sleep Timeouts

OptionDefaultDescription
sleepTimeout30 secondsTime of inactivity before the actor begins sleeping.
sleepGracePeriod15 secondsTotal graceful shutdown window for hooks, waitUntil, keepAwake, async raw WebSocket handlers, and disconnects.

These can be configured in the options.

WebSocket Hibernation

WebSocket hibernation is in beta and disabled by default.

When WebSocket hibernation is enabled, connections are preserved across sleep cycles and transparently migrated to the new actor instance. Client stays connected and sees no interruption. Actor migration is very fast, realtime workloads are not interrupted.

Shutdown Sequence

When an actor sleeps or is destroyed, it enters the graceful shutdown window:

  1. c.abortSignal fires and c.aborted becomes true. New connections and dispatch are rejected. Alarm timeouts are cancelled. On sleep, scheduled events are persisted and will be re-armed when the actor wakes.
  2. onSleep or onDestroy and onDisconnect for each closing connection run during the same window. User waitUntil promises and async raw WebSocket handlers are drained. Hibernatable WebSocket connections are preserved on sleep and closed on destroy.
  3. Once graceful work has completed, state is saved and final cleanup runs.

The entire window is bounded by sleepGracePeriod on both sleep and destroy. Defaults to 15 seconds. If the window is exceeded, the actor proceeds to state save anyway.

Graceful shutdown window

During steps 1 and 2, the actor is still fully functional. Database access, broadcast, waitUntil, and schedule.after all work. Actions invoked by pre-existing connections or lifecycle hooks continue to execute normally.

New connections and raw WebSocket upgrades are rejected as soon as the shutdown sequence begins. New requests that arrive during shutdown are held until the actor wakes up again. The caller does not need to retry.

If schedule.after is called during shutdown, the event is persisted so it survives the sleep/wake cycle, but no local timeout is scheduled. The event will fire after the actor wakes.

In-flight actions are not waited on during shutdown. If an action must complete before the actor stops, wrap the critical work with c.waitUntil().

Options

The options object allows you to configure various timeouts and behaviors for your actor.

import { actor } from "rivetkit";

const myActor = actor({
  options: {
    // Timeout for createVars function (default: 5000ms)
    createVarsTimeout: 5000,

    // Timeout for onConnect hook (default: 5000ms)
    onConnectTimeout: 5000,

    // Total graceful shutdown budget for both sleep and destroy. Default: 15000ms.
    sleepGracePeriod: 15_000,

    // Timeout for action execution (default: 60000ms)
    actionTimeout: 60_000,

    // Timeout for connection liveness check (default: 2500ms)
    connectionLivenessTimeout: 2500,

    // Interval for connection liveness check (default: 5000ms)
    connectionLivenessInterval: 5000,

    // Time before actor sleeps due to inactivity (default: 30000ms)
    sleepTimeout: 30_000,

    // Whether WebSockets can hibernate for onWebSocket (default: false)
    // Can be a boolean or a function that takes a Request and returns a boolean
    canHibernateWebSocket: false,
  },

  actions: { /* ... */ }
});
OptionDefaultDescription
createVarsTimeout5000msTimeout for createVars function
onConnectTimeout5000msTimeout for onConnect hook
sleepGracePeriod15000msTotal graceful shutdown window for both sleep and destroy
actionTimeout60000msTimeout for action execution
connectionLivenessTimeout2500msTimeout for connection liveness check
connectionLivenessInterval5000msInterval for connection liveness check
sleepTimeout30000msTime before actor sleeps due to inactivity
canHibernateWebSocketfalseWhether WebSockets can hibernate (experimental)

Advanced

Actor Shutdown Abort Signal

The c.abortSignal provides an AbortSignal that fires when the actor is stopping, and c.aborted is the shorthand boolean for loop checks. Use these to cancel ongoing operations when the actor sleeps or is destroyed.

The abort signal fires at the very start of the shutdown sequence, before onSleep or onDestroy runs. This means c.aborted is already true inside those lifecycle hooks. The signal fires early so that the run workflow and background tasks can exit promptly, but the actor remains fully functional throughout the graceful shutdown window.

import { actor } from "rivetkit";

const chatActor = actor({
  actions: {
    generate: async (c, prompt: string) => {
      const response = await fetch("https://api.example.com/generate", {
        method: "POST",
        body: JSON.stringify({ prompt }),
        signal: c.abortSignal
      });

      return await response.json();
    }
  }
});

See Canceling Long-Running Actions for manually canceling operations on-demand.

Using ActorContext Type Externally

When extracting logic from lifecycle hooks or actions into external functions, you’ll often need to define the type of the context parameter. Rivet provides helper types that make it easy to extract and pass these context types to external functions.

import { actor, ActorContextOf } from "rivetkit";

const myActor = actor({
  vars: { startedAt: 0 },
  actions: {},
});

// Simple external function with typed context
function logActorStarted(c: ActorContextOf<typeof myActor>) {
  console.log(`Actor started at: ${c.vars.startedAt}`);
}

See Types for more details on using ActorContextOf.

Full Example

import { type ActorContextOf, actor } from "rivetkit";
import { db } from "rivetkit/db";

interface CounterInput {
  initialCount?: number;
  stepSize?: number;
  name?: string;
}

interface ConnParams {
  userId: string;
  role: string;
}

interface ConnInfo {
  userId: string;
  role: string;
  connectedAt: number;
}

const counter = actor({
  // Durable data lives in SQLite
  db: db({
    onMigrate: async (db) => {
      await db.execute(`
        CREATE TABLE IF NOT EXISTS counter (
          id INTEGER PRIMARY KEY CHECK (id = 1),
          count INTEGER NOT NULL,
          step_size INTEGER NOT NULL,
          name TEXT NOT NULL,
          request_count INTEGER NOT NULL DEFAULT 0
        );
      `);
    },
  }),

  // Seed the database from input. This runs only once per actor.
  onCreate: async (c, input: CounterInput) => {
    await c.db.execute(
      "INSERT INTO counter (id, count, step_size, name) VALUES (1, ?, ?, ?)",
      input.initialCount ?? 0,
      input.stepSize ?? 1,
      input.name ?? "Unnamed Counter",
    );
    console.log(`Counter "${input.name}" initialized`);
  },

  // Ephemeral data, rebuilt empty on every start
  createVars: () => ({
    connections: new Map<string, ConnInfo>(),
    // `onWake` stores its task here so `onSleep` can wait for it.
    reporter: null as Promise<void> | null,
  }),

  // Lifecycle hooks
  onWake: async (c) => {
    const [{ name, count }] = await c.db.execute<{ name: string; count: number }>(
      "SELECT name, count FROM counter WHERE id = 1",
    );
    console.log(`Counter "${name}" started with count:`, count);

    // Start a background task without blocking startup
    c.vars.reporter = reportCount(c).catch((error) => {
      c.log.error({ msg: "count reporter failed", error });
    });
  },

  // `c.abortSignal` fires before `onSleep`, so the task is already exiting
  onSleep: async (c) => {
    await c.vars.reporter;
  },

  onBeforeConnect: (c, rawParams) => {
    // Validate connection params
    const params = rawParams as ConnParams;
    if (!params.userId) {
      throw new Error("userId is required");
    }
    console.log(`User ${params.userId} attempting to connect`);
  },

  // Track each connection in memory, keyed by connection ID
  onConnect: (c, conn) => {
    const { userId, role } = conn.params as ConnParams;
    c.vars.connections.set(conn.id, { userId, role, connectedAt: Date.now() });
    console.log(`User ${userId} connected`);
  },

  onDisconnect: (c, conn) => {
    const info = c.vars.connections.get(conn.id);
    c.vars.connections.delete(conn.id);
    console.log(`User ${info?.userId} disconnected`);
  },

  // Observe action responses before they are sent, counting them in SQLite so the total survives sleep
  onBeforeActionResponse: async (c, actionName, args, output) => {
    await c.db.execute("UPDATE counter SET request_count = request_count + 1 WHERE id = 1");
    console.log(`Action ${actionName} called`, args);
    return output;
  },

  // Define actions
  actions: {
    increment: async (c, amount?: number) => {
      // Add the amount, or the stored step size when none is passed, in one atomic statement
      const [{ count, name }] = await c.db.execute<{ count: number; name: string }>(
        "UPDATE counter SET count = count + COALESCE(?, step_size) WHERE id = 1 RETURNING count, name",
        amount ?? null,
      );

      // React to the change right where it happens
      c.broadcast("countUpdated", { count, name });
      return count;
    },

    getInfo: async (c) => {
      const [info] = await c.db.execute<{
        name: string;
        count: number;
        stepSize: number;
        totalRequests: number;
      }>(
        "SELECT name, count, step_size AS stepSize, request_count AS totalRequests FROM counter WHERE id = 1",
      );
      return info;
    },
  }
});

export default counter;

// Log the count every minute until the actor starts shutting down
async function reportCount(c: ActorContextOf<typeof counter>): Promise<void> {
  while (!c.aborted) {
    const [{ name, count }] = await c.db.execute<{ name: string; count: number }>(
      "SELECT name, count FROM counter WHERE id = 1",
    );
    console.log(`Counter "${name}" is at ${count}`);
    await delay(60_000, c.abortSignal);
  }
}

// Resolve after `ms`, or as soon as the actor starts shutting down
function delay(ms: number, signal: AbortSignal): Promise<void> {
  return new Promise((resolve) => {
    if (signal.aborted) {
      resolve();
      return;
    }

    const finish = () => {
      clearTimeout(timer);
      signal.removeEventListener("abort", finish);
      resolve();
    };
    const timer = setTimeout(finish, ms);
    signal.addEventListener("abort", finish, { once: true });
  });
}
Edit this page Last updated November 10, 2026