# Replicas and autoscaling
URL: https://synquic.tech/docs/guides/replicas.md

Run several copies of an app on one server with the edge balancing requests across them, and let Synq add or remove copies with the load.

import { Step, Steps } from 'fumadocs-ui/components/steps';

A replica is a full copy of your app running next to the original on the same server. With three
replicas, Synq runs three identical containers (same image, environment, mounts and networks) and the
[edge](/docs/guides/the-edge) spreads incoming requests across them in turn. If one copy stops answering,
the edge skips it and retries the request on the next one, so a crash or a restart of one replica does not
turn into an error page.

Replicas help when one container is the bottleneck: a single-threaded app saturating one CPU core, a
worker that should consume a queue in parallel, or an app you want to keep serving while one copy restarts.
They do not add servers: every replica shares the machine's CPU, memory and disk.

<Callout title="What you need first">
- A **single-app** project deployed with **Docker** to one of your own servers. Compose stacks and monorepo
  services are not replicated (see [Limits](#limits)).
- Permission to change the project's settings.
</Callout>

## Set the number of replicas

<Steps>

<Step>

### Open Resources

Open the project, choose **Resources** in the project menu and open the **Replicas** card.

</Step>

<Step>

### Pick a count

Use the plus and minus buttons (or type a number from 1 to 20) and press **Apply**. The change applies to
the running deployment right away, without a rebuild:

- **Adding replicas:** each new copy is started from the running container, must stay up through a short
  stabilization window (and answer the project's readiness check, when one is configured), and only then
  joins the edge's list of targets.
- **Removing replicas:** the highest-numbered copies leave the edge's list first, are then stopped gracefully
  (the app receives its stop signal and Docker's grace period to finish in-flight requests) and removed.

**1** turns replicas off. The count is saved on the project, so the next deploy and any rollback run the same
number.

</Step>

<Step>

### Check them

**Running now** lists every replica with its container and state. The project's logs show all replicas
together, each line prefixed with `[replica N]`, and the usage card lists each replica's CPU and memory with
the total across all of them.

</Step>

</Steps>

From the CLI:

```bash
synq project replicas proj_123          # show the count, each replica and recent scale events
synq project replicas proj_123 3        # run three copies now
synq project replicas proj_123 3 --no-apply   # save for the next deploy only
```

Over the API, `GET /api/projects/:id/replicas` reads the same view and `PUT /api/projects/:id/replicas` with
`{ "replicas": 3 }` changes it (see the [projects reference](/docs/api/projects)). The same operations are
available as MCP tools.

## How requests are balanced

The project's domains point at an nginx `upstream` group with one member per replica, in round-robin order.
Each member is checked passively: one failed connection or timeout takes it out of rotation for ten seconds,
and the request is retried on the next member. Only when every replica fails does the visitor see the edge's
"application unavailable" page.

Linked apps that reach this one over the private network use its internal hostname, which every replica
answers to, so Docker's DNS spreads those calls across the replicas too.

There is no session affinity: consecutive requests from one visitor can land on different replicas. Keep
sessions in a shared store (a database, Redis) or in signed cookies.

## Deploys with replicas

A deploy starts the new release's replicas next to its first container, checks each one, and only then points
the edge at the new set. The previous release's replicas keep serving until the switch and are then drained
and removed. A replica that fails to start fails the deploy before any traffic moves, and the previous
release keeps running. Pausing, resuming and restarting the project act on every replica.

## Volumes

Named volumes and bind mounts in the app's [persistent storage](/docs/guides/persistent-storage) are mounted by
**every** replica at the same time. That is fine for uploaded files or a cache, and unsafe for a SQLite file or
anything only one process may write. The Replicas card lists the shared volumes and asks you to confirm before
running more than one copy; the API needs `"allowSharedVolumes": true`. Volumes an image declares without a
name are created per container, so each replica has its own.

## What cannot run replicas

| Reason | Why |
| --- | --- |
| The project runs services (compose or monorepo) | Replicas are per single-app project. |
| It is a static site | The edge serves the files; there is no container to copy. |
| It uses the bare runtime, Synq Cloud or a cluster | Replicas need Docker on a server. Clusters have their own replica count. |
| It publishes fixed host ports ([Ports](/docs/guides/ports)) | Only one container can hold a host port. Remove the published ports; the edge still reaches every replica. |
| Zero-downtime deploys are off | That setting says the app must never run twice at once. |

Publishing a host port on an app that runs replicas is refused the same way.

## Autoscaling

Autoscaling moves the replica count between a minimum and a maximum with the load. Turn it on in the
**Autoscaling** section of the Replicas card, or from the CLI:

```bash
synq project replicas proj_123 --autoscale on --min 2 --max 6 --cpu 70
synq project replicas proj_123 --memory 80      # also scale on memory
synq project replicas proj_123 --autoscale off
```

The settings are the **minimum** and **maximum** replicas (up to 20), a **CPU target** in percent (70 by
default) and an optional **memory target**. Every 30 seconds Synq reads each replica's usage (the same
`docker stats` reading the usage card shows) and averages it across the replicas:

- **CPU** is measured against each replica's CPU limit, or one core when the app has no limit (a
  single-threaded app is saturated at 100%).
- **Memory** is measured against each replica's memory limit, or the server's memory when there is none.

It then applies these rules:

| Rule | Effect |
| --- | --- |
| The average stays **above** the target (CPU, or memory when set) for **2 minutes** | Add one replica. |
| Every watched metric stays **below half** its target for **10 minutes** | Remove one replica. |
| Less than **3 minutes** since the last change (manual or automatic) | Do nothing yet. |
| The count is outside the minimum and maximum | Bring it back at once. |
| The server lacks the free memory another replica needs | Do not add it; report why. |

The memory needed for another replica is the app's memory limit, or the busiest replica's current usage when it
has none. When the server is short, the card shows the reason, a **Not scaled** event is recorded and a
notification is sent once; the replica is added as soon as memory frees up while the load lasts.

New replicas pass the same checks as a manual change before the edge sends them traffic, and removed ones are
drained the same way.

With autoscaling on, a number you set by hand is the starting point and is kept between the minimum and
maximum. A deploy runs the current count, so an app that was scaled up stays scaled up through a release.

### Scale history and alerts

The card's **Recent scale events** list keeps the last 20 changes with their reason, for example
`CPU 91% > 70% for 2m`, whether they came from you or from the autoscaler. Every automatic change is also written
to the [audit log](/docs/guides/audit-log) as **App autoscaled** (a refused scale-up as **Autoscaling blocked**),
and sent through [notifications](/docs/guides/notifications) under **App autoscaled**, which is on by default.

## Limits

- **Compose stacks and monorepo services** are not replicated. Scale the stack's app into its own single-app
  project to use replicas.
- **One server.** Replicas share the server's resources and each one gets the app's full CPU and memory
  limits, so three replicas of a 1 GB app can use 3 GB.
- **Single apps have no terminal,** so there is no replica picker for exec; service terminals are per
  service.
- A replica that is stopped is left out of the edge's list until it runs again. Scaling or redeploying
  recreates any replica that disappeared.
