Back to Rivet

Versions & Upgrades

website/src/content/docs/actors/versions.mdx

2.3.59.0 KB
Original Source

How Versions Work

Each runner has a version number. When you deploy new code with a new version, Rivet handles the transition automatically:

  • New actors go to the newest version: When allocating actors, Rivet always prefers runners with the highest version number
  • Multiple versions can coexist: Old actors continue running on old versions while new actors are created on the new version
  • Drain old actors: When enabled, a runner connecting with a newer version number will gracefully stop old actors to be rescheduled to the new version
<Note> Versions are not configured by default. See [Registry Configuration](/docs/general/registry-configuration) to learn how to configure the runner version. </Note> <Note> `RIVET_ENVOY_VERSION` is only needed when self-hosting or using a custom runner. Rivet Compute handles versioning automatically. </Note>

Example Scenario

<Tabs> <Tab title="Drain Enabled">

When a new version is deployed, existing actors are gracefully stopped on the old runner and rescheduled onto the new version.

mermaid
sequenceDiagram
    participant R1 as Runner v1
    participant R2 as Runner v2

    Note over R1: Currently running
    Note over R2: Deployed
	R2->>R1: Drain old actors
    R1->>R2: Reschedule actors
    Note over R1: Shut down when all actors stopped
</Tab> <Tab title="Drain Disabled">

When a new version is deployed, both versions coexist. New actors are created on the new version while existing actors continue running on the old version until.

mermaid
sequenceDiagram
    participant R1 as Runner v1
    participant R2 as Runner v2

    Note over R1: Currently running
    Note over R2: Deployed
	Note over R1: Actor 1 sleeps from inactivity
	Note over R2: Actor 1 wakes up when prompted
</Tab> </Tabs>

Configuration

Setting the Version

Configure the runner version using an environment variable or programmatically:

<CodeGroup>
bash
RIVET_ENVOY_VERSION=2
<CodeSnippet file="examples/docs/actors-versions/programmatic-version.ts" title="Programmatic" /> </CodeGroup> <Warning> The version **must** be set at build time, not at runtime. Do not use `Date.now()` or similar runtime values in your registry setup code. This would assign a different version every time the server starts, causing actors to be drained and rescheduled on every restart instead of only on new deployments. </Warning>

Example Configurations

We recommend injecting a build-time value that increments with every deployment. Here are concrete examples for common setups:

<Tabs> <Tab title="Dockerfile">

Generate the version at build time and bake it into the image as an environment variable:

bash
docker build --build-arg RIVET_ENVOY_VERSION=$(date +%s) .
dockerfile
FROM node:20-slim
ARG RIVET_ENVOY_VERSION
ENV RIVET_ENVOY_VERSION=$RIVET_ENVOY_VERSION
WORKDIR /app
COPY . .
RUN npm install && npm run build
CMD ["node", "dist/server.js"]

All containers from this image will share the same version.

</Tab> <Tab title="Next.js">

Set the version in next.config.ts. Next.js evaluates this file once at build time and inlines the value into the bundle:

typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  env: {
    RIVET_ENVOY_VERSION: String(Math.floor(Date.now() / 1000)),
  },
};

export default nextConfig;
</Tab> <Tab title="Vite">

Use define in your Vite config. This is evaluated once at build time and inlined into the bundle:

typescript
import { defineConfig } from "vite";

export default defineConfig({
  define: {
    "process.env.RIVET_ENVOY_VERSION": JSON.stringify(
      String(Math.floor(Date.now() / 1000))
    ),
  },
});
</Tab> <Tab title="CI/CD">

Set the version from your CI pipeline:

yaml
# GitHub Actions
env:
  RIVET_ENVOY_VERSION: ${{ github.run_number }}
bash
# Railway / Render / generic CI
export RIVET_ENVOY_VERSION=$(date +%s)
bash
# Git commit count
export RIVET_ENVOY_VERSION=$(git rev-list --count HEAD)
</Tab> <Tab title="Build Script">

Generate a version file during your build step and import it:

json
{
  "scripts": {
    "build": "echo \"export const BUILD_VERSION = $(date +%s);\" > src/build-version.ts && tsc"
  }
}
typescript
import { actor, setup } from "rivetkit";
import { BUILD_VERSION } from "./build-version";

const myActor = actor({ state: {}, actions: {} });

const registry = setup({
  use: { myActor },
  envoy: {
    version: BUILD_VERSION,
  },
});
</Tab> </Tabs>

Drain on Version Upgrade

The drainOnVersionUpgrade option controls whether old actors are stopped when a new version is deployed. This is configured in the Rivet dashboard under your runner configuration. See Pool Configuration for the full set of pool options, including how to rate-limit actor eviction during the drain.

ValueBehavior
falseOld actors continue running. New actors go to new version. Versions coexist.
true (default)Old actors receive stop signal and have 30m to finish gracefully.

Upgrading Actor State

When you deploy a new version, existing actors may need to handle schema changes in their persisted data.

Drizzle (recommended)

Use Drizzle for typed schemas with generated migrations. Drizzle generates versioned .sql migration files from your TypeScript schema and applies them in order automatically. This is the recommended approach when your schema evolves frequently.

Raw SQL

For actors using raw SQLite, migrations run automatically via the onMigrate hook on every actor start. RivetKit wraps the hook in a SQLite savepoint, so the migration is fully atomic. Use SQLite's user_version pragma to track which migrations have run:

<CodeSnippet file="examples/docs/actors-versions/sqlite-migrate.ts" />

In-memory state (c.state)

If you use c.state for persistence, you are responsible for handling schema changes yourself. If you add, remove, or rename fields between versions, your code must handle the old shape gracefully.

Manual defaults in onWake

Apply defaults for missing fields:

<CodeSnippet file="examples/docs/actors-versions/onwake-defaults.ts" />

Zod schema coercion

Use Zod to parse persisted state on wake. Zod's .default() fills in missing fields automatically, so old actor state is coerced to the current schema:

<CodeSnippet file="examples/docs/actors-versions/zod-coercion.ts" />

For anything beyond simple defaults, consider moving to SQLite where you get proper migration tooling.

Advanced

How Version Upgrade Detection Works

When drainOnVersionUpgrade is enabled, Rivet uses two mechanisms to detect version changes:

  • New runner connection: When a runner connects with a newer version number, the engine immediately drains all older runners with the same name. This is the primary mechanism for runner mode deployments.
  • Metadata polling (serverless only): In serverless mode, runners periodically poll the engine to check for newer versions and self-drain if one is found. This ensures old runners drain even if no new requests trigger a runner connection.

SIGTERM Handling

When a runner process receives SIGTERM, it gracefully stops all actors before exiting:

  • Each actor's onSleep hook is called, giving it time to save state
  • Actors are rescheduled to other available runners
  • The runner waits up to 30 minutes for all actors to finish stopping
  • If the process is force-killed before actors finish (e.g. SIGKILL), actors are rescheduled with a crash backoff penalty instead of a clean handoff
<Note> Actors have a maximum of 30 minutes to clean up during shutdown. Ensure your platform's drain grace period is at most 30 minutes. </Note>

Shutdown Timeouts

Several timeouts control how long each part of the shutdown process can take:

TimeoutDefaultDescriptionConfiguration
actor_stop_threshold30mEngine-side limit on how long each actor has to stop before being marked lostEngine config (pegboard.actor_stop_threshold)
sleepGracePeriod15sTotal graceful sleep budget for onSleep, waitUntil, keepAwake, and async raw WebSocket handlersActor options
runner_lost_threshold15sFallback detection if the runner dies without graceful shutdownEngine config (pegboard.runner_lost_threshold)

Rivet has a max shutdown grace period of 30 minutes that cannot be configured.