From 891a22f91511d3c36aadd1881f944f0aa011db47 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Sun, 3 May 2026 15:46:32 -0700 Subject: [PATCH] docs(rivetkit): raise serverless start payload limit --- examples/kitchen-sink/package.json | 1 + examples/kitchen-sink/src/index.ts | 6 ++++++ .../packages/rivetkit-core/src/engine_process.rs | 12 ++++++++++++ .../packages/rivetkit/src/registry/config/index.ts | 8 ++++++-- .../rivetkit/src/registry/config/serverless.ts | 7 ++++++- website/src/content/docs/actors/limits.mdx | 2 +- .../content/docs/general/production-checklist.mdx | 1 + 7 files changed, 33 insertions(+), 4 deletions(-) diff --git a/examples/kitchen-sink/package.json b/examples/kitchen-sink/package.json index 24a55ab07a..17989c6948 100644 --- a/examples/kitchen-sink/package.json +++ b/examples/kitchen-sink/package.json @@ -6,6 +6,7 @@ "packageManager": "pnpm@10.13.1", "scripts": { "dev": "concurrently -n server,vite \"node --import @rivetkit/sql-loader --import tsx src/server.ts\" \"vite\"", + "dev:serverless": "MALLOC_ARENA_MAX=2 MALLOC_TRIM_THRESHOLD_=131072 RIVET_RUN_ENGINE=1 concurrently -n server,vite \"node --import @rivetkit/sql-loader --import tsx src/server.ts\" \"vite\"", "check-types": "echo 'skipped - workflow history types broken'", "build": "vite build", "test": "node --import tsx --test tests/*.test.ts", diff --git a/examples/kitchen-sink/src/index.ts b/examples/kitchen-sink/src/index.ts index 96f908d8fc..4027f4cf39 100644 --- a/examples/kitchen-sink/src/index.ts +++ b/examples/kitchen-sink/src/index.ts @@ -164,6 +164,12 @@ function serverlessPoolConfig() { export const registry = setup({ configurePool: serverlessPoolConfig(), + serverless: { + maxStartPayloadBytes: numberFromEnv( + "RIVET_SERVERLESS_MAX_START_PAYLOAD_BYTES", + 16 * 1024 * 1024, + ), + }, use: { // Overview + state basics counter, diff --git a/rivetkit-rust/packages/rivetkit-core/src/engine_process.rs b/rivetkit-rust/packages/rivetkit-core/src/engine_process.rs index fc8b0b5e25..0d1a997f0e 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/engine_process.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/engine_process.rs @@ -12,6 +12,8 @@ use crate::error::EngineProcessError; const ENGINE_RUNTIME: &str = "engine"; const RIVETKIT_RUNTIME: &str = "rivetkit"; +const DEFAULT_MALLOC_ARENA_MAX: &str = "2"; +const DEFAULT_MALLOC_TRIM_THRESHOLD: &str = "131072"; #[derive(Debug, Deserialize)] struct EngineHealthResponse { @@ -110,6 +112,7 @@ impl EngineProcessManager { .stdin(Stdio::null()) .stdout(Stdio::from(stdout_file)) .stderr(Stdio::from(stderr_file)); + set_default_allocator_env(&mut command); // Put the engine in its own process group so terminal signals // (Ctrl+C, Ctrl+Z, SIGHUP on terminal close) targeting our foreground @@ -176,6 +179,15 @@ impl EngineProcessManager { } } +fn set_default_allocator_env(command: &mut Command) { + if std::env::var_os("MALLOC_ARENA_MAX").is_none() { + command.env("MALLOC_ARENA_MAX", DEFAULT_MALLOC_ARENA_MAX); + } + if std::env::var_os("MALLOC_TRIM_THRESHOLD_").is_none() { + command.env("MALLOC_TRIM_THRESHOLD_", DEFAULT_MALLOC_TRIM_THRESHOLD); + } +} + impl Drop for EngineProcessManager { fn drop(&mut self) { if let Some(handle) = self.watcher.take() { diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts b/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts index d70cdfe588..682cd3f583 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts @@ -25,7 +25,11 @@ import { isDev, } from "@/utils/env-vars"; import { EnvoyConfigSchema } from "./envoy"; -import { ConfigurePoolSchema, ServerlessConfigSchema } from "./serverless"; +import { + ConfigurePoolSchema, + DEFAULT_SERVERLESS_MAX_START_PAYLOAD_BYTES, + ServerlessConfigSchema, +} from "./serverless"; export const ActorsSchema = z.record( z.string(), @@ -505,7 +509,7 @@ export const DocServerlessConfigSchema = z .number() .optional() .describe( - "Maximum POST /start body size in bytes. Default: 1048576", + `Maximum POST /start body size in bytes. Default: ${DEFAULT_SERVERLESS_MAX_START_PAYLOAD_BYTES}`, ), publicEndpoint: z .string() diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/config/serverless.ts b/rivetkit-typescript/packages/rivetkit/src/registry/config/serverless.ts index f2ffe1891e..7064be4665 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/config/serverless.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/config/serverless.ts @@ -1,6 +1,8 @@ import { z } from "zod/v4"; import { getRivetPublicEndpoint, getRivetPublicToken } from "@/utils/env-vars"; +export const DEFAULT_SERVERLESS_MAX_START_PAYLOAD_BYTES = 16 * 1024 * 1024; + export const ConfigurePoolSchema = z .object({ name: z.string().optional(), @@ -17,7 +19,10 @@ export const ConfigurePoolSchema = z export const ServerlessConfigSchema = z.object({ // MARK: Routing basePath: z.string().optional().default("/api/rivet"), - maxStartPayloadBytes: z.number().optional().default(1_048_576), + maxStartPayloadBytes: z + .number() + .optional() + .default(DEFAULT_SERVERLESS_MAX_START_PAYLOAD_BYTES), // MARK: Public Endpoint Configuration /** diff --git a/website/src/content/docs/actors/limits.mdx b/website/src/content/docs/actors/limits.mdx index b19622b409..2db22bca09 100644 --- a/website/src/content/docs/actors/limits.mdx +++ b/website/src/content/docs/actors/limits.mdx @@ -93,7 +93,7 @@ These limits apply to the [SQLite database](/docs/actors/state#sqlite-database) ### KV Preloading -When an actor starts, the engine can pre-fetch KV data declared in the actor name metadata and deliver it alongside the start command. This removes round-trips to storage during actor startup. RivetKit emits the preload manifest from its own key layout and exposes per-actor overrides via `options`. Operators can still enforce a global cap in the [engine config](/docs/self-hosting/configuration) with `pegboard.preload_max_total_bytes`. +When an actor starts, the engine can pre-fetch KV data declared in the actor name metadata and deliver it alongside the start command. This removes round-trips to storage during actor startup. RivetKit emits the preload manifest from its own key layout and exposes per-actor overrides via `options`. Operators can still enforce a global cap in the [engine config](/docs/self-hosting/configuration) with `pegboard.preload_max_total_bytes`. In serverless mode, this data is serialized into the `/api/rivet/start` request body along with actor config and protocol metadata, so the accepted body size must be larger than the preload budget. RivetKit defaults `serverless.maxStartPayloadBytes` to 16 MiB to leave margin for the default 1 MiB preload budget and larger SQLite startup page preloads. | Name | Soft Limit | Hard Limit | Description | |------|------------|------------|-------------| diff --git a/website/src/content/docs/general/production-checklist.mdx b/website/src/content/docs/general/production-checklist.mdx index 341a13b9e1..2a5fe62297 100644 --- a/website/src/content/docs/general/production-checklist.mdx +++ b/website/src/content/docs/general/production-checklist.mdx @@ -21,6 +21,7 @@ We recommend passing this page to your coding agent to verify your configuration ### Serverless - **Check platform timeouts.** Rivet handles migration between invocations automatically, but shorter timeouts increase migration frequency. See [Timeouts](/docs/general/runtime-modes#timeouts). +- **Verify `/api/rivet/start` body size limits.** Serverless actor starts carry actor config and preloaded KV or SQLite startup data in the request body. Keep `serverless.maxStartPayloadBytes` and your platform or proxy body limit at **16 MiB or higher**, or lower the preload budget if your platform cannot accept that size. See [Limits](/docs/actors/limits#kv-preloading). - **Configure max runners.** Go to Settings > Providers > Edit Provider > Max Runners to set the limit. The default is 100,000 runners. This is effectively your max actor count. ### Runner