Skip to content
BurnerByte

Capacity and Sizing

What one machine runs, the limits the shipped defaults impose, the one component that cannot scale out yet, and which number to raise first.

What you are actually running

A default deployment is six long-running containers on one host: PostgreSQL, Redis, MinIO, the Go API, the Go SMTP daemon and the Next.js frontend. A seventh, the migration runner, executes once and exits. None of them is memory-hungry on its own, and the database is the only one whose appetite grows with your data rather than with your traffic.

There is no published benchmark for this project, and this page will not invent one. Throughput depends on your mail volume, your attachment sizes and your disk more than on anything in the code. What can be stated precisely is what the shipped configuration allows before it starts refusing work — which is usually the number you actually want when sizing a box.

Note

The limits the defaults impose

SettingDefaultWhat it bounds
database.max_open_conns25Postgres connections held by one API process
database.max_idle_conns5Connections kept open between requests
smtp.workers4Messages parsed concurrently
smtp.queue_size1000Messages accepted and waiting on those workers
smtp.max_size25 MiBLargest message the daemon will accept
defaults.max_attachment_size_mb25Largest attachment stored
defaults.max_inboxes_per_domain100Live inboxes on one domain
defaults.max_domains10Domains on the instance
defaults.max_teams50Teams in the organization
defaults.default_inbox_ttl10mTTL an inbox gets when none is chosen
defaults.max_inbox_ttl24hLongest an inbox can live
rate_limit.authenticated300/minRequests per user
rate_limit.unauthenticated60/minRequests per IP

The two worth thinking about before you deploy are smtp.queue_size and smtp.workers. Together they are your burst tolerance: a thousand messages can be accepted while four are being parsed, and a sender arriving at a full queue is refused rather than kept waiting. If your traffic is bursty — a test suite that sends a few hundred messages in a few seconds — raise the queue before you raise anything else.

Disk is the thing that grows

Mail and attachments are the only data that accumulates without bound during normal operation, and attachments are much the larger of the two. At the default 25 MiB ceiling, a hundred inboxes each receiving a handful of attachment-carrying messages is measured in gigabytes rather than megabytes.

Inboxes expire and the cleanup worker deletes them along with their mail and attachments, so steady-state usage is governed by your TTLs rather than by how long the instance has been up. An instance where most inboxes use the 24-hour maximum holds roughly a day of traffic; one left on the ten-minute default holds almost nothing. The audit log and analytics rollups do accumulate indefinitely and are small.

The ceiling that is not a number

The API binary is the constraint, and not for any reason you can configure away. Every API process starts all seven background workers and there is no leader election, so a second replica does not share the load — it duplicates it, producing doubled webhook retries and doubled analytics rollups.

Run exactly one API replica

The SMTP daemon has no such restriction. It holds no timers and owns no periodic work — it accepts mail, parses it, stores it and publishes to Redis — so it can be replicated freely behind whatever is balancing port 25 or 2525. If inbound mail is your bottleneck, that is the part to add instances of.

Which number to raise first

Raise these in roughly this order, because each one stops mattering until the one before it is comfortable:

  1. smtp.queue_size, if senders are being refused during bursts.
  2. smtp.workers, if the queue drains too slowly — this is CPU-bound parsing, so it wants cores.
  3. database.max_open_conns, if requests are waiting on the pool. Raise PostgreSQL's own max_connections to match; the default 100 leaves room for one API process at 25 and not many more.
  4. Disk, before any of the above, if attachments are the story.

What to watch

The Prometheus metrics and the /healthz and /readyz endpoints described in Monitoring are how you find out which of these you have hit, rather than guessing. Queue depth and database pool saturation are the two that tell you something is about to be refused, and both move before anything visibly breaks.

If something is already failing rather than merely tight, the symptoms and their causes are collected in Troubleshooting, and the hardening checklist in Production Deployment covers the settings that matter before this instance faces the internet.