Skip to content

Running more than one instance

Most of a Rebase deployment already lives in the database: rows, users and sessions, API keys, the job queue, cron claims, idempotency keys, record history, the MCP OAuth server’s tokens. A second process pointed at the same database sees all of it.

A few things do not. Each is per process by default, because one process is the default deployment and sharing costs something — a table, a bucket write, a database connection. Running two processes behind a load balancer, autoscaling past one, or a rolling deploy that briefly runs the old and new side by side all put you on this page. So does a split deployment, which is several processes by definition.

Go through the list below before the second one takes traffic. Nothing on it fails loudly: each item shows up as a limit enforced three times over, an event some clients never see, or a page that shows different logs on every refresh.

What Per process by default What shares it
Signing secrets Generated per process in development JWT_SECRET and REBASE_SERVICE_KEY, set explicitly and identical everywhere
Rate-limit counters In memory REBASE_RATE_LIMIT_STORE=sql
Client address behind a proxy TRUSTED_PROXY_HOPS=0 The number of proxies in front, the same everywhere
Collection subscriptions Shared through the database when CDC is on REALTIME_CDC=auto (the default)
Broadcast channels and presence In memory REALTIME_CHANNEL_BUS=postgres
Uploaded files, STORAGE_TYPE=local The instance’s own disk S3 or GCS, or one shared volume at STORAGE_PATH
Resumable (TUS) uploads in progress The instance’s memory and disk Sticky sessions; a restart still loses them
Image transforms An in-process cache STORAGE_RENDITION_CACHE=true
Logs Explorer A ring of the last 10,000 lines Nothing — ship stdout to a log aggregator
Cron timers Every process that runs the scheduler Claimed in the database: one run per slot. REBASE_CRON_SCHEDULER picks where timers live
Scheduled RLS audit Every process that owns it REBASE_RLS_AUDIT=false on all but one
A static app’s index.html Read once per process Restart every instance when the build changes
/metrics Each process counts its own Scrape every instance

The sections below say what each one does when it is left per process.

JWT_SECRET signs every session and REBASE_SERVICE_KEY authenticates server-to-server calls. In development each process generates its own when they are unset, so a token one process issued is refused by the next. Production already refuses to boot without them; what matters with several processes is that every one gets the same values — from one secret, not one per replica.

The rate limiters — the per-caller budget on the data, storage and functions APIs, and the auth limiters on sign-in, password reset, one-time codes and MFA attempts — count in memory by default. A process cannot see how many peers it has, so three replicas on the default enforce three times every limit. Set REBASE_RATE_LIMIT_STORE=sql and the counters live in the database instead.

Behind a load balancer the limiter also needs the real client address, which arrives in X-Forwarded-For. TRUSTED_PROXY_HOPS says how many proxies to look past; at the default 0 every request appears to come from the load balancer and every client shares one bucket. See Configuration.

Collection subscriptions work across instances when database-level change capture is on, which it is by default (REALTIME_CDC=auto): a trigger announces each committed write, and every instance’s listener refetches for its own subscribers. If CDC is off, or auto could not provision it (the boot log says which), a subscription sees only the writes made through the instance its socket is connected to. See Realtime.

Broadcast channels and presence are in-process unless a bus carries them: two clients on different instances in the same channel do not hear each other, and each instance answers “who is here?” with its own half. Set REALTIME_CHANNEL_BUS=postgres. The bus listens on the database, which needs a direct connection rather than a transaction pooler — set DATABASE_DIRECT_URL when DATABASE_URL goes through pgBouncer. See Channels and presence across instances.

With STORAGE_TYPE=local, uploads are files on the disk of the instance that received them, and another instance answers 404 for them. Use S3 or GCS, or mount one volume at STORAGE_PATH on every instance. See Self-hosting: file storage.

Resumable uploads (the TUS endpoint) keep each upload’s partial file and its state on the local disk of the instance that created it, under STORAGE_PATH/.tus-uploads — even when finished files go to S3 or GCS. A chunk that lands on another instance is answered 404 and the client starts again. Route a client’s upload requests to one instance (sticky sessions on the load balancer). A shared volume at STORAGE_PATH shares the partial files but not yet the upload state, which is held in the process’s memory — so a restart or a rolling deploy also sends an upload in progress back to byte 0. Ordinary uploads through POST /upload are one request and are not affected.

Image transforms (?width=400&format=webp) are cached in memory, so every instance computes every variant once, and a new instance starts cold. Set STORAGE_RENDITION_CACHE=true to write each rendition back to the bucket it came from, where every instance finds it. That makes a GET write to your bucket, which is why it is off unless asked for. See Storage.

Studio’s Logs Explorer reads a ring of the last 10,000 log lines kept by the process that serves the request. Behind a load balancer each refresh may show a different instance’s lines, and none of them shows the whole deployment. The explorer names the instance it is showing. There is no setting that shares the ring: the runtime writes one JSON line per event to stdout in production, and that is what to collect — your platform’s log service, Loki, or anything that reads container output.

Every process that runs the cron scheduler arms its own timers, and the run is claimed in the database first, so a slot runs once however many processes fire for it. Pausing a job, and the lease that keeps a manual trigger off a run in progress, are shared the same way. Nothing to set, as long as the database is Postgres. REBASE_CRON_SCHEDULER and REBASE_JOB_WORKERS decide which processes run timers and workers at all — see Split processes.

The scheduled RLS audit is the exception: it is not claimed, so every process that owns it scans on its own timer. That is redundant rather than unsafe; set REBASE_RLS_AUDIT=false everywhere but one.

A process that serves the frontend or the CMS (REBASE_SERVE_STATIC, on by default) reads each app’s index.html once and keeps it. Replacing the build on a shared volume does not reach a running process: it keeps serving the old document, which names chunks that may no longer exist. Ship a new build by restarting or rolling every instance — which a new image or bundle does anyway. With a CDN in front and REBASE_SERVE_STATIC=false, this does not apply.

/metrics reports the process that answers it. Scrape each instance — a Prometheus service-discovery job per pod, not one target behind the load balancer — and sum in the query.

Every instance runs the additive schema pass at boot (REBASE_MIGRATE_ON_BOOT=ensure). Instances of the same role may run it at the same time: it is written to tolerate a peer creating the same table a moment earlier. In a split deployment, exactly one role provisions and every other sets none — see Split processes.