website/src/content/docs/actors/versions.mdx
Each runner has a version number. When you deploy new code with a new version, Rivet handles the transition automatically:
When a new version is deployed, existing actors are gracefully stopped on the old runner and rescheduled onto the new version.
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
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.
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
Configure the runner version using an environment variable or programmatically:
<CodeGroup>RIVET_ENVOY_VERSION=2
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:
docker build --build-arg RIVET_ENVOY_VERSION=$(date +%s) .
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:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
env: {
RIVET_ENVOY_VERSION: String(Math.floor(Date.now() / 1000)),
},
};
export default nextConfig;
Use define in your Vite config. This is evaluated once at build time and inlined into the bundle:
import { defineConfig } from "vite";
export default defineConfig({
define: {
"process.env.RIVET_ENVOY_VERSION": JSON.stringify(
String(Math.floor(Date.now() / 1000))
),
},
});
Set the version from your CI pipeline:
# GitHub Actions
env:
RIVET_ENVOY_VERSION: ${{ github.run_number }}
# Railway / Render / generic CI
export RIVET_ENVOY_VERSION=$(date +%s)
# Git commit count
export RIVET_ENVOY_VERSION=$(git rev-list --count HEAD)
Generate a version file during your build step and import it:
{
"scripts": {
"build": "echo \"export const BUILD_VERSION = $(date +%s);\" > src/build-version.ts && tsc"
}
}
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,
},
});
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.
| Value | Behavior |
|---|---|
false | Old actors continue running. New actors go to new version. Versions coexist. |
true (default) | Old actors receive stop signal and have 30m to finish gracefully. |
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:
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:
For anything beyond simple defaults, consider moving to SQLite where you get proper migration tooling.
When drainOnVersionUpgrade is enabled, Rivet uses two mechanisms to detect version changes:
When a runner process receives SIGTERM, it gracefully stops all actors before exiting:
onSleep hook is called, giving it time to save stateSeveral timeouts control how long each part of the shutdown process can take:
| Timeout | Default | Description | Configuration |
|---|---|---|---|
actor_stop_threshold | 30m | Engine-side limit on how long each actor has to stop before being marked lost | Engine config (pegboard.actor_stop_threshold) |
sleepGracePeriod | 15s | Total graceful sleep budget for onSleep, waitUntil, keepAwake, and async raw WebSocket handlers | Actor options |
runner_lost_threshold | 15s | Fallback detection if the runner dies without graceful shutdown | Engine config (pegboard.runner_lost_threshold) |
Rivet has a max shutdown grace period of 30 minutes that cannot be configured.
onSleep