Managed backend runtime
A backend app declared like this in rebase.json runs on the managed
runtime:
{ "apps": { "backend": { "type": "backend", "runtime": "managed" } }}Managed means you ship a bundle, not an image. rebase cloud deploy builds your
collections, backend/functions/ and crons into dist-bundle, uploads it, and
Rebase Cloud boots it on the published rebasepro/server image. You never write
a Dockerfile, and you also cannot change what is in the image. This page lists
what that allows, so you can plan a feature before you build it.
The numbers here come from the runtime and the platform as they are today. Some are dials you can change, and the page says which ones. The few things that are not promised yet are collected at the end.
At a glance
Section titled “At a glance”| Question | Answer |
|---|---|
| Node version | Node 22, on Debian slim, linux/x64 |
| npm packages | Yes, from the public registry or a private one. Installed into the bundle when you deploy |
Native modules (.node, node-gyp) |
No. rebase cloud deploy refuses the bundle |
| WebAssembly | Yes |
| Spawn a child process | Yes, for a linux/x64 binary an npm package ships. See Binaries |
| Memory and CPU | Set per project with rebase cloud compute. See Compute |
| Instances | 1 by default, always on. You can pin up to 5, or let the autoscaler go up to 16 |
| Function timeout | 30 seconds, then 504 FUNCTION_TIMEOUT. Configurable up to 10 minutes |
| Request body | 10 MiB, then 413. Configurable, up to the 64 MiB the edge accepts |
| Writable filesystem | Yes, but it is temporary and belongs to one instance |
Module-level state (a Map cache) |
Survives between requests on one instance. Lost on every deploy and restart, and not shared between instances |
| Environment variables and secrets | rebase cloud env set, applied at the next deploy |
| File storage | A bucket per project, served by your backend at /api/storage/file/… |
| Outbound HTTP | Yes, on ports 80 and 443 (and 465, 587, 993 for mail). Other ports are blocked. One fixed outbound IP: 207.175.138.15 |
Code and packages
Section titled “Code and packages”Node and the image. The runtime image is built on node:22-slim
(Debian), and your bundle runs on linux/x64 as a non-root user. Your
tsconfig and the Node version on your machine do not change this.
Dependencies. rebase cloud deploy installs the dependencies your
package.json files declare into the bundle, for linux/x64, and uploads
them with it, so a new instance has nothing to install. A bundle that would pass
the 1 GB upload limit with them is uploaded without them, and the deploy says
so: each instance then installs them as it starts, with npm ci (or
npm install without a lockfile). Four things follow from how they install:
-
Install scripts never run. The install uses
--ignore-scripts. A package that downloads a binary or builds something inpostinstallinstalls without it. -
Only registry versions install. A
file:,link:,git+…orgithub:dependency fails the build, because the runtime has nothing to resolve it against. -
Some packages come from the image, not from you.
@rebasepro/server,@rebasepro/types,@rebasepro/client,@rebasepro/common,@rebasepro/utils,hono,@hono/node-serverandzodare removed from the bundle’s dependencies and the image’s own copies are linked in, so your version of them does not apply. Other@rebasepro/*packages, such as the database driver, install from yourpackage.jsonlike any dependency.typescriptandtsxare removed too; they are build tools. -
Private packages need an
.npmrc.rebase buildcopies the project’s.npmrc(andbackend/.npmrc) into the bundle, so the install uses the same registry settings as your machine. Every credential in it must be an environment reference, never the token itself; a literal token fails the build, because the bundle is uploaded with your code:@acme:registry=https://npm.pkg.github.com//npm.pkg.github.com/:_authToken=${NPM_TOKEN}rebase cloud env set NPM_TOKEN=ghp_… --secret
The bundle upload is capped at 1 GB, compressed.
Native modules are refused. A package that ships a compiled .node binary
cannot load on the managed runtime. rebase build warns about it, and
rebase cloud deploy stops before uploading:
This bundle depends on native modules (sharp), which the managed runtime cannot runThe check looks for well-known native packages (sharp, canvas, bcrypt,
argon2, sqlite3, better-sqlite3, re2, sodium-native, pg-native, …),
for binding.gyp or gypfile, for install scripts that call node-gyp or
prebuild, and for .node files. The
deployment guide
lists the usual ways around it. The short version: do the work at build time,
call a service, or deploy with runtime: "custom".
WebAssembly works. A package that ships a .wasm file and loads it from
JavaScript is not a native module, and Node 22 runs it. Checked in the
runtime’s base image with the pod’s security settings, using esbuild-wasm.
Remember the memory limit if the module is large,
such as a speech or ML model.
Binaries and child processes
Section titled “Binaries and child processes”child_process works. The image itself has very little to run: node, npm,
sh, tar, and the PostgreSQL client tools (pg_dump, pg_restore). There is
no ffmpeg, no ImageMagick, no Python and no compiler, and you cannot add
system packages, because there is no Dockerfile and the process is not root.
A binary that an npm package carries inside its own tarball installs with
the package and runs. It must be built for linux/x64 against glibc
(Debian), and it runs as an unprivileged user with no Linux capabilities.
Checked with @esbuild/linux-x64 in the runtime’s base image. A binary that a
package downloads in postinstall never arrives, because install scripts do
not run; packages like ffmpeg-static work that way.
If a feature needs a binary, use a hosted API for that work, or move the
backend to runtime: "custom", where you build your own image.
Compute and instances
Section titled “Compute and instances”Each project runs in its own pods. Memory and CPU are per instance, and you set them with the compute dials:
rebase cloud compute # what the project reserves nowrebase cloud compute set --cpu 500m --memory 1GiA project that never set a dial runs as a burstable pod: it reserves 100m CPU
and 384Mi memory, and can burst to 2 CPU and 2 GiB. A dialled request keeps
that ratio between request and limit. Run rebase cloud compute to see what
your project has.
Instances:
- One by default, always running.
--replicaspins between 1 and 5. Nothing scales a backend to zero. - Autoscaling is opt-in.
--autoscale-maxsets a ceiling of up to 16, and--autoscale-cpu-target(default 70) the CPU use the autoscaler aims for. - Deploys roll. A new instance starts and becomes ready before the old one stops, so for a short time two versions serve traffic.
--spot is off by default; spot instances are cheaper and restarted without
notice.
Memory and module-level state
Section titled “Memory and module-level state”A module-level value, like const cache = new Map() at the top of a function
file, lives in the Node process. It survives from one request to the next on
the same instance. It is lost whenever that process ends:
- on every deploy, because each deploy starts new instances;
- when an environment variable changes and is deployed;
- when an instance is restarted, rescheduled or (with
--spot) reclaimed.
With more than one instance, each one has its own copy, so two requests can see different caches.
Use a module-level Map as a cache you can afford to lose: one that makes the
next request faster but is not the only copy of anything. Keep anything you
paid to produce, like generated audio or an expensive API answer, in the
database or in storage. For clients you build
once, such as an API SDK, use lazyResource so the client is built on first
use from the request’s environment. See
Configuration and Secrets.
Running more than one instance lists the state Rebase itself keeps per process, such as rate-limit counters and realtime channels.
The filesystem
Section titled “The filesystem”The container’s filesystem is writable, and /tmp works. But everything
written there:
- belongs to one instance, and other instances do not see it;
- is deleted when the instance stops, so at the latest on the next deploy;
- shares a 6 GiB ephemeral-storage limit with the bundle and its installed
node_modules. An instance that goes over is evicted.
Use it for scratch files during one request. Do not use it as a cache across
deploys or as storage for uploads. That is also why
STORAGE_TYPE=local is turned off in production.
Requests
Section titled “Requests”| Limit | Default | Change it with |
|---|---|---|
| Function timeout | 30 s, then 504 with code FUNCTION_TIMEOUT |
REBASE_FUNCTIONS_TIMEOUT_MS (0 turns it off) |
| Waiting on your instance, at the edge | 10 minutes between reads or writes | Fixed |
Request body, /api/* routes without a limit of their own |
10 MiB, then 413 PAYLOAD_TOO_LARGE |
REBASE_MAX_BODY_SIZE, in bytes |
| Request body at the edge | 64 MiB | Fixed |
One file through POST /api/storage/upload |
50 MB | STORAGE_MAX_FILE_SIZE |
Anonymous calls to /api/functions/* |
3000 per IP per 15 minutes | rateLimit.anonymousFunctions |
Some details:
- The timeout frees the caller, not the work. At 30 seconds the caller gets
a
504, andgetSignal(c)is aborted. Pass that signal tofetchand anything else that accepts one, so the work stops too. See Timeouts and Rate Limits. - The edge waits longer than the runtime. The ingress in front of your
instances waits up to 10 minutes, so the limit a request hits is the runtime’s,
which answers JSON and aborts the signal. Raise
REBASE_FUNCTIONS_TIMEOUT_MSup to that. For work that takes longer, or that must survive a deploy (an instance drains for 15 seconds), answer at once and do the work in a job. - Uploads larger than 64 MiB need the resumable (TUS) upload, which sends
the file in chunks.
client.storage.putObject()switches to it on its own above 8 MiB. - Response size. Neither the runtime nor the edge sets a limit on response size. A large response is still held in memory if your handler builds it in one piece, so stream it or serve it from storage.
Environment variables and secrets
Section titled “Environment variables and secrets”Set them with the CLI or in the console. There is no field for them in
rebase.json.
rebase cloud env set TTS_API_URL=https://api.example.com/v1/speechrebase cloud env set TTS_API_KEY=sk-… --secret # write-only: never shown againrebase cloud env list # names only, never valuesrebase cloud deploy # applies the change- A change applies at the next deploy. Running instances keep the values
they started with.
setandunsetremind you of this (Pending redeploy). - Limits: up to 100 variables per project, 32 KiB per value and 256 KiB in
total. Names follow
[A-Za-z_][A-Za-z0-9_]*. - Some names are reserved. The platform sets
DATABASE_URL,DATABASE_DIRECT_URL,DATABASE_READ_URL,JWT_SECRET,REBASE_SERVICE_KEY,PORTandNODE_ENVitself, and pins how the process runs (REBASE_ROLE,TRUSTED_PROXY_HOPS, …). Setting one of those is refused, and the error lists them all. CORS_ORIGINSstarts from your domains. The platform sets it to the project’s host and its verified custom domains. ACORS_ORIGINSyou set is added to that list, for a frontend served from somewhere else:rebase cloud env set CORS_ORIGINS=https://app.example.org.- The storage variables are defaults. The platform writes the
STORAGE_*andS3_*variables for its bucket, and a value you set wins. - Build-time names are refused.
VITE_,NEXT_PUBLIC_,PUBLIC_andREACT_APP_variables are for frontends at build time, and the backend never sees them in time.--forcestores one anyway.
In a function, read them inside the handler, never at the top of the file:
import { defineFunction, requireEnv } from "@rebasepro/server/functions";
export default defineFunction((app) => { app.get("/status", async (c) => { const endpoint = requireEnv(c, "TTS_API_URL"); // throws a clear error if unset return c.json({ configured: Boolean(endpoint) }); });});A module-scope process.env read that comes back empty makes the whole file
fail to load, and its routes answer 404. See
Configuration and Secrets.
Storage for generated files
Section titled “Storage for generated files”A Cloud project gets file storage once you create it:
rebase cloud storage create # a managed bucket and its credentialsrebase cloud deploy # the running backend gets the credentials at deployOr point the project at a bucket you already own with
rebase cloud storage attach (S3, R2, MinIO, any S3-compatible bucket).
Without either, uploads answer 501 STORAGE_NOT_CONFIGURED. More buckets are
declared in config/resources.ts; see
Several Buckets.
A deploy with storage is refused until the project says who may read and write
files. The scaffold’s config/storage.ts exports a storageAuthorize hook for
this. See Per-Object Authorization.
How files are served. Your backend serves every file at
GET /api/storage/file/<key>. It reads the object from the bucket and streams
it, with Content-Type, range requests and caching headers. There is no direct
bucket URL to hand out.
- A key under
public/is served to anyone, without a token, at a URL that never changes. Use it for files any visitor may load, such as generated audio for a public game. - Any other key is private. A client asks
/api/storage/metadata/<key>for a download URL that holds a token, valid for 300 seconds by default (STORAGE_DOWNLOAD_TOKEN_TTL, up to a week).
From a function, write with rebase.storage (the default bucket) or
rebase.bucket(…) (a named one). Your storageAuthorize hook still decides
whether the write is allowed.
Image transforms (?width=, ?format=) need sharp, which is a native module
and is not in the managed image. On the managed runtime, a transform request
gets the original image.
Example: cache generated audio
Section titled “Example: cache generated audio”A function that turns text into speech with a hosted API, stores each result
once, and serves it from storage afterwards. The Map only saves a storage
lookup on a warm instance; the bucket is the real cache, so a deploy loses
nothing.
import { defineFunction, getSignal, requireEnv } from "@rebasepro/server/functions";
const known = new Map<string, true>(); // per instance, lost on deploy: that is fine
export default defineFunction((app, { rebase }) => { app.get("/voice/:id", async (c) => { const id = c.req.param("id").replace(/[^a-z0-9-]/gi, ""); const key = `public/voices/${id}.mp3`; const url = `/api/storage/file/${key}`;
if (known.has(key)) return c.redirect(url); const storage = rebase.storage; if (!storage) return c.json({ error: "Storage is not configured" }, 501);
const listed = await storage.listObjects(key, { maxResults: 1 }); if (listed.items.some(item => item.fullPath === key)) { known.set(key, true); return c.redirect(url); }
const text = c.req.query("text") ?? ""; const response = await fetch(requireEnv(c, "TTS_API_URL"), { method: "POST", headers: { authorization: `Bearer ${requireEnv(c, "TTS_API_KEY")}`, "content-type": "application/json" }, body: JSON.stringify({ text }), signal: getSignal(c) }); if (!response.ok) return c.json({ error: "Speech service failed" }, 502);
const file = new File([await response.arrayBuffer()], `${id}.mp3`, { type: "audio/mpeg" }); await storage.putObject({ file, key }); known.set(key, true); return c.redirect(url); });});The route is public, like every route in backend/functions/ until you guard
it. A real one would check who may create new audio, so that a stranger cannot
spend your speech-API budget.
Outbound network
Section titled “Outbound network”Functions can call external services. Outbound connections from your instances are allowed to any internet address on these TCP ports only:
| Port | For |
|---|---|
| 80, 443 | HTTP and HTTPS |
| 465, 587 | Sending mail over SMTP with TLS or STARTTLS |
| 993 | IMAP over TLS |
Everything else is blocked. That includes port 25, plain IMAP, and databases or
caches on their own ports: Redis on 6379 or MongoDB on 27017 outside Rebase
Cloud cannot be reached. The cloud metadata address (169.254.169.254) is
blocked too.
The one exception is your own database. If the project brings its own PostgreSQL (BYODB), each deploy also allows connections to that database’s host on the port in its connection string, and to nothing else on that port. The host is resolved to its IP addresses when you deploy, so if your provider moves the database to another address, deploy again. The database must accept connections from the internet: the app connects to the connection string directly. The SSH bastion settings are used only by the connection test, not by the deployed app.
The platform adds no proxy and no outbound rate limit; the services you call
have their own. Pass getSignal(c) to fetch so a call stops when its request
times out.
All outbound traffic leaves from one fixed IP address, 207.175.138.15.
Add it to a third-party allowlist when a service asks for one. The address is
shared by every project on Rebase Cloud, so an allowlist proves the caller runs
on Rebase Cloud, not that it is your project: keep authenticating to the
service with a key as well.
Startup and shutdown
Section titled “Startup and shutdown”- Cold start. A new instance downloads and unpacks the bundle before it serves, so a large dependency tree makes every scale-up slower. A bundle uploaded without its dependencies installs them as well, which adds 40 to 60 seconds. The platform waits up to 300 seconds for an instance to come up.
- Health. The platform checks
/livez(the process is running) and/health(it can reach the database). Both are served by the runtime, so you do not write them. - Shutdown. On a deploy or restart, the instance stops taking new requests,
waits 5 seconds for traffic to move, and then gets
SIGTERM. Requests in flight have up to 15 seconds to finish. Promises passed towaitUntilget up to 5 seconds of that. A promise you did not hand towaitUntilis dropped. See Background Work.
When managed is the wrong fit
Section titled “When managed is the wrong fit”Move the backend to "runtime": "custom" when it needs:
- a native module, or a system binary no npm package ships, such as
sharp, ImageMagick or a local speech engine; - system packages or a different base image;
- a long-lived connection to something outside the allowed ports.
A custom runtime builds your own Dockerfile, and you own the image from then
on. rebase cloud deploy --eject makes the switch. See
Rebase Cloud.
Not promised yet
Section titled “Not promised yet”- Log history.
rebase cloud logs --runtimereads from the instances that are running, andrebase cloud debug logs --previousfrom the last run of one that restarted. Output from an instance a deploy replaced is gone. Write anything you need to keep to the database or to a log service of your own. - Scale to zero is not offered. Every backend runs at least one instance at all times.
Related pages
Section titled “Related pages”- Rebase Cloud: linking, deploying, the compute dials, and what the beta does not include
- Custom Functions: writing the routes in
backend/functions/ - Runtime and Bundles: what a bundle is and how the runtime boots it
- Storage: buckets, endpoints, size limits and access rules
- Running more than one instance: the state Rebase keeps per process