# Sizing a self-hosted deployment

Source: https://codeherder.com/docs/self-host-sizing/

Start-up sizes for the app host, PostgreSQL, and device fleet of a self-hosted server, where each number comes from, and the signals that tell you to grow.

This page gives you a starting size for each part of a self-hosted deployment. Every number comes from the CodeHerder hosted service. The sources are named in each section.

## How to read these numbers

These numbers are a starting point. They are not a load test of your workload. Your team size, repository size, and session count decide your real needs. You own the workload definition and the thresholds for adding capacity.

Watch two signals from the first day:

- **Pool wait alerts** show that PostgreSQL connections are the limit.
- **Device memory** shows that sessions are the limit. Watch RAM on each device.

## App host

| Item | Starting value | Source |
| --- | --- | --- |
| Instance | 2 vCPU, 2 GB RAM, burstable (`t4g.small`) | The hosted service’s app host |
| Process memory in use | About 700 MB resident | Measured on the hosted service before it moved to this instance |
| Average CPU | 6 to 9 percent | Same measurement |
| Go memory limit | `GOMEMLIMIT=1200MiB` | Set on the hosted service through a systemd drop-in |

`GOMEMLIMIT` is a soft limit on the Go heap. It does not cap resident memory. It makes the garbage collector work harder as the heap nears the limit.

Grow the host when resident memory stays above 1.2 GB, or when CPU stays above the burst baseline. On the same instance family, a larger size buys memory only. The vCPU count and burst baseline do not change between the two smallest sizes.

## PostgreSQL

| Item | Starting value | Source |
| --- | --- | --- |
| Instance | 2 vCPU, 4 GB RAM (`db.t4g.medium`) | The hosted service’s database |
| Storage | 50 GB, growing automatically to at most 200 GB | Same |
| `max_connections` | 200 | Same |
| Server pool defaults | Reader 8, writer 8, analytics 2 | The server’s built-in defaults |
| Hosted pool sizes | Reader 32, writer 12, analytics 4 | Chosen on the hosted service from 30 days of pool-wait metrics, August 2026 |

Set the three pools with `CH_READ_POOL`, `CH_WRITE_POOL`, and `CH_ANALYTICS_POOL`. You can also use the `--read-pool`, `--write-pool`, and `--analytics-pool` flags. The full list is in the server configuration reference that ships with your release.

Do not size the reader pool from the number of connections in use. A short burst can block many requests and be over before the next sample. Watch the pool wait alert instead.

**Connection budget.** Add up every connection that can be open at once. The hosted service runs two app hosts during an upgrade, so it budgets for two:

| Consumer | Connections |
| --- | --- |
| One app host: 32 + 12 + 4 | 48 |
| Two app hosts | 96 |
| Other services that share the database | 10 |
| Operators and restore checks | 5 |
| Reserved for the superuser | 3 |
| Managed-service overhead | 3 |
| **Total** | **117 of 200** |

If you run one app host and stop it before you start the new one, halve the app host line. Keep the total under `max_connections`.

Storage grows with task history, events, and costs. The server prunes old events and cost rows on a schedule. Grow storage when free space falls below 20 percent.

## Device fleet

| Item | Starting value | Source |
| --- | --- | --- |
| Instance | 4 vCPU, 16 GiB RAM (`m9g.xlarge`) | The default of the CodeHerder AWS device template |
| Sessions per device | 8 | Default per-device limit and default worktree cap |
| Data disk | 50 GB | Default of the AWS device template |

Agent sessions are limited by memory, not by CPU. On 15 August 2026, a device with 32 GiB of RAM that worked on a very large repository stalled four times in one afternoon at 92 percent RAM or more. A device that worked on a small repository used 13 percent of its RAM with four times as many sessions.

So there is no fixed number of sessions per GiB. Start from these steps:

1. Run the default of 8 sessions on the starting instance.
2. Watch RAM while your largest repository builds.
3. If RAM goes above 80 percent, lower **Max concurrent sessions**, or move to a memory-heavy instance such as `r8g.2xlarge` with 64 GiB.
4. Give the device swap space. A device with no swap does not slow down when memory runs out. It stalls.

Each session has its own git worktree on the data volume. Size the disk for your largest repository, multiplied by the worktree cap. Add the build cache (20 GiB by default). See [Self-hosted device disk growth](https://codeherder.com/docs/self-host-device-disk/) for the measured growth and the alert level. Use `--session-disk-budget-mb` to enforce a limit. See [Running the device server as a service](https://codeherder.com/docs/running-the-device-server/) and [Managing your devices](https://codeherder.com/docs/devices/).

## Headroom and expansion triggers

Every trigger below has a signal, a place to read it, a **Warn** level and an **Expand** level. At Warn, plan the change. At Expand, make the change before the next working day. Write your own lead time in the last column: how long the change takes you from request to done. Set Warn early enough that the lead time fits.

### Where the load numbers come from

The database numbers come from the load test CodeHerder ran on 1 October 2026 against the [launch workload](https://codeherder.com/docs/self-host-workload/). It raised the load in steps of 1, 2, 5, 10, 20, 40, 64 and 100 times the launch profile. The test used a database with 2 CPUs, pools of 8 reader, 8 writer and 2 analytics connections, and one host for the server and the load generator.

- At 5 times the launch load, the reader pool waited 17 ms in total over 45 seconds. The 95th percentile did not change.
- At 10 times, the reader pool waited on every sample period. That is the first sign of a limit.
- At 40 times, the 95th percentile was still under 13 ms. The waits had no visible effect.
- At 64 times, the 95th percentile reached 83 ms. Users start to notice.
- At 100 times, the server refused requests.
- The writer pool never waited. Its peak was 5 of 8 connections.

**Headroom rule.** Keep your peak load at or under 5 times the launch profile on the starting sizes. **Expansion trigger.** Plan to expand at the behaviour seen at 10 times: pool wait on most sample periods. This is far below the 40 times where waits are harmless and the 64 times where users see them. Your database, network and workload differ. Rerun the load test on your own setup before you rely on these ratios.

The launch workload was measured at 30 concurrent sessions. The load test did not saturate live attach sockets, imports or attachment downloads. Treat the socket rows below as unmeasured.

### App host

| Signal | Where to read it | Warn | Expand | Step | Your lead time |
| --- | --- | --- | --- | --- | --- |
| CPU | Your host metrics. On a burstable instance, read the CPU credit balance. | Average above 50 percent of the baseline for a day, or the credit balance falls for 3 days in a row | Credit balance near zero, or CPU at the baseline for an hour | Move to a larger or non-burstable instance |  |
| Resident memory | Your host metrics for the server process | Above 1.0 GB for a day | Above 1.2 GB (the `GOMEMLIMIT` value) for an hour | Move to the next size. Check `GOMEMLIMIT` after the move. |  |
| Sockets | Open file descriptors of the server process, against its limit. Live attach sockets and realtime subscriptions match the workload profile (8 and 16 at launch). | Descriptors above 50 percent of the limit | Above 80 percent of the limit | Raise the file limit in the service unit, then move to a larger host |  |

### PostgreSQL

| Signal | Where to read it | Warn | Expand | Step | Your lead time |
| --- | --- | --- | --- | --- | --- |
| Reader pool wait | The `db pool stats` log line every `CH_DB_STATS_LOG_SEC` seconds (60 by default): `deltaWaitCount` and `deltaWaitMs` for the line with `pool=reader`. Also `GET /v1/instance/metrics/db`. | `deltaWaitCount` above 0 on most ticks for a day | The pool wait alert: `deltaWaitMs` above `CH_ALERT_POOL_WAIT_MS` (1000) on `CH_ALERT_POOL_SAT_TICKS` (3) ticks in a row | Raise `CH_READ_POOL`. Check the connection budget first. |  |
| Analytics pool wait | Same line, analytics pool | `deltaWaitMs` above 1000 on most ticks | `CH_ALERT_ANALYTICS_POOL_WAIT_MS` (5000) on `CH_ALERT_ANALYTICS_POOL_SAT_TICKS` (10) ticks in a row | Raise `CH_ANALYTICS_POOL`, or move the cost and report load to a read replica |  |
| Writer pool wait | Same line, writer pool | Any `deltaWaitCount` above 0 | Waits on 3 ticks in a row | Look for a long transaction first. Then raise `CH_WRITE_POOL`. The load test never made the writer wait. |  |
| Connections | `SELECT count(*) FROM pg_stat_activity`, against `max_connections` | Above 70 percent of `max_connections` | Above 85 percent | Lower other consumers, or raise `max_connections` and the instance memory with it |  |
| Storage | Free space on the database volume | Below 30 percent free | Below 20 percent free | Raise the storage limit, or prune history sooner |  |
| Disk I/O | `pg_stat_io` read and write times, or your volume’s IOPS and latency metrics | Read latency above 10 ms for an hour, or IOPS above 70 percent of the volume limit | Read latency above 20 ms for a day, or IOPS at the limit | Move to a volume with more IOPS, or a larger instance with more memory for the cache |  |

The connection and I/O levels are common PostgreSQL practice. The load test did not measure them. Review them after your first month of data.

### Device fleet

| Signal | Where to read it | Warn | Expand | Step | Your lead time |
| --- | --- | --- | --- | --- | --- |
| Session slots | The **Session capacity** readiness check on each device. The age of tasks that wait for a device. | Slots in use above 75 percent on most devices at the busy hour | Tasks wait for a slot for more than 15 minutes, with every device full | Add a device, or raise **Max concurrent sessions** if RAM allows |  |
| Device RAM | `ch device metrics <device>` (RAM %) | Above 70 percent during builds | Above 80 percent | Lower **Max concurrent sessions**, or move to a larger instance |  |
| Device disk | `diskPct` from `ch device metrics <device>` | 70 percent | 85 percent | Clean the device, then enlarge the volume. See [Self-hosted device disk growth](https://codeherder.com/docs/self-host-device-disk/). |  |

Set your own formal thresholds, owners and review dates in your operator runbook. A fleet of laptops needs a longer lead time than a fleet of cloud hosts. Write the owner of each row and how often that person checks it.

## Related guides

- [Self-hosted deployment](https://codeherder.com/docs/self-hosting/) — download, start, and upgrade the server
- [Self-hosted launch workload](https://codeherder.com/docs/self-host-workload/) — the load to size for
- [Self-hosted backup and recovery](https://codeherder.com/docs/self-host-backups/) — protect what you size here
- [Self-hosted device disk growth](https://codeherder.com/docs/self-host-device-disk/) — measure, alert on and clean device disk use
- [Managing your devices](https://codeherder.com/docs/devices/) — capacity limits and readiness checks
- [Running the device server as a service](https://codeherder.com/docs/running-the-device-server/) — worktree and disk flags
