Capacity and scaling
How many chat turns one Liyagent VM serves today, what limits it first, and the steps that take an instance to ten and a hundred times that load.
- Size one VM for your chat volume
- Know which limit you will reach first
- Plan the move to several app replicas
This page gives measured numbers for one Liyagent instance and the order in which its limits appear as chat volume grows. It also lists the steps that raise each limit, so you can plan capacity before you need it.
How we measured
We ran one app process with its own PostgreSQL 16 database and the embedded vector store, as the single-VM install runs them. The machine had 18 cores. The model was a stub that answers after a fixed delay, so the numbers show what the platform costs and not what a model provider costs. We tried delays of 0.3 seconds, 1.5 seconds and 5 seconds.
Each simulated person signed in with their own account, kept one chat session, and sent 10, 25 or 50 messages with about 2 seconds of think time between them. Real people usually think for much longer than 2 seconds, so each simulated person stands for several real ones. Every scenario stopped sending new messages after 4 minutes.
No request failed in any scenario. When the instance was busy, people waited longer instead.
Results with a 1.5 second model
These runs started from an empty database.
| People | Messages each | Median reply | 95th percentile | 99th percentile | Replies per second |
|---|---|---|---|---|---|
| 10 | 10 | 2.4 s | 3.6 s | 4.0 s | 2.1 |
| 25 | 10 | 1.9 s | 2.3 s | 3.0 s | 5.7 |
| 50 | 10 | 2.3 s | 3.2 s | 3.8 s | 10.2 |
| 100 | 10 | 9.5 s | 14.2 s | 16.7 s | 8.1 |
| 10 | 25 | 1.8 s | 1.8 s | 1.9 s | 2.5 |
| 25 | 25 | 1.8 s | 2.2 s | 2.4 s | 6.1 |
| 50 | 25 | 4.1 s | 8.0 s | 9.6 s | 7.3 |
| 100 | 25 | 12.4 s | 24.3 s | 28.8 s | 6.4 |
| 10 | 50 | 1.8 s | 2.0 s | 3.4 s | 2.5 |
| 25 | 50 | 2.1 s | 6.0 s | 6.7 s | 4.9 |
| 50 | 50 | 7.5 s | 14.3 s | 17.5 s | 4.8 |
| 100 | 50 | 16.1 s | 29.4 s | 37.2 s | 5.1 |
The first 10 person run includes the warm-up of a freshly started process, which is why it is slower than the 25 person run.
Results with other model speeds
| Model delay | People | Median reply | 95th percentile | Replies per second |
|---|---|---|---|---|
| 0.3 s | 10 | 0.5 s | 0.6 s | 3.6 |
| 0.3 s | 25 | 0.6 s | 0.7 s | 8.0 |
| 0.3 s | 50 | 1.9 s | 3.1 s | 11.5 |
| 0.3 s | 100 | 7.9 s | 14.2 s | 9.1 |
| 5 s | 10 | 5.2 s | 5.3 s | 1.3 |
| 5 s | 25 | 5.2 s | 5.3 s | 3.3 |
| 5 s | 50 | 5.3 s | 6.3 s | 6.0 |
| 5 s | 100 | 11.7 s | 16.1 s | 6.6 |
Results on an instance that has been running for a while
An instance keeps up to 5,000 recent conversations in its transcript list. Once that list is close to full, every chat turn costs more. We repeated the 10 message runs on a database that already held about 4,100 conversations.
| People | Median reply | 95th percentile | 99th percentile | Replies per second |
|---|---|---|---|---|
| 10 | 4.3 s | 5.8 s | 6.5 s | 1.5 |
| 25 | 12.4 s | 17.9 s | 20.5 s | 1.6 |
| 50 | 26.8 s | 37.9 s | 44.0 s | 1.6 |
| 100 | 57.2 s | 70.9 s | 82.0 s | 1.6 |
What one VM serves today
The platform adds about 0.25 seconds to each reply on a new instance and about 2.7 seconds on an instance whose transcript list is nearly full.
One app process completes at most about 10 chat turns a second on a new instance and about 1.6 a second on a long running one. If people pause about 30 seconds between messages, that is roughly 300 people chatting at the same moment on a new instance and roughly 50 on a long running one. Plan with the long running figure, because every busy instance reaches it.
Adding CPU cores to the VM does not raise these numbers. The app runs as one process, and that process uses about one core for chat work.
Which limit you reach first
- The transcript list. All conversations of the instance share one list, and every chat turn reads and rewrites it several times. The cost of a turn grows with the number of kept conversations, up to the 5,000 cap. This is the limit a long running instance meets first.
- Worker threads. Each chat turn holds one of 40 worker threads for its whole length, including the time spent waiting for the model. With a slow model, 40 turns in flight is the ceiling. At a 5 second model this shows as about 40 calls open at the model and replies queueing behind them.
- One CPU core. With a fast model, the process spends about 90 ms of CPU on each turn, which caps it near 10 turns a second.
- Session summaries. When a chat passes 30 exchanges, the oldest turns are folded into a summary by an extra model call. That call happens before the reply, so it adds one model call to the turn that triggers it, and again about every 10 exchanges after that.
- The per caller rate limit. Each signed in person may send 30 messages a minute by default (
TICKETIQ_RATE_LIMIT_EXPENSIVE). Callers that the instance cannot tie to a person, such as Google Chat events and anonymous web chat visitors, share the limit of their network address.
The database was not the limit in any run. It stayed below 55 connections and was mostly idle while the app process did its work.
Settings you can change today
| Setting | When to change it |
|---|---|
TICKETIQ_TRANSCRIPT_MAX_CONVERSATIONS | Lower it (for example to 1,000) to keep chat turns fast on a busy instance. Fewer recent conversations stay visible in the console. |
TICKETIQ_RATE_LIMIT_EXPENSIVE | Raise it when a Google Chat space or a web chat behind one office network sends more than 30 messages a minute. |
TICKETIQ_QDRANT_URL | Point it at a Qdrant server to move the vector store out of the app process. This is required before you run more than one app process. |
TICKETIQ_TEAMS_QUEUE_CONCURRENCY | Raise it from 4 when Teams replies wait in the inbox while the model is idle. |
| Per agent timeout | Give agents that answer people a shorter model timeout, so one slow provider does not hold worker threads for a full minute. |
The path to ten times the load
Ten times means about 100 turns a second, or roughly 3,000 people chatting at the same moment. These steps come in order.
- Store transcripts and session lists as database rows. Each turn then touches only its own conversation, so a turn costs the same on a long running instance as on a new one. This alone moves a long running instance from about 1.6 to about 10 turns a second.
- Size worker threads and database connections together. Raise the worker thread count above 40, and replace one connection per thread with a connection pool. Put PgBouncer in session mode in front of PostgreSQL, because the app uses session advisory locks.
- Move the summary fold after the reply, so a long chat never waits for it.
- Run the vector store as a Qdrant server instead of inside the app.
- Run several app replicas behind the load balancer. Today a second process against the same database refuses to start, because sign-ins in flight, rate limit counters, the live event stream and several write locks are held in one process's memory. Each of these moves to PostgreSQL first. Scheduled jobs already run on one elected process, so they need no change.
- Give PostgreSQL its own machine or a managed service with at least 4 cores, and set
max_connectionsfor the pool size of all replicas together.
The path to a hundred times the load
A hundred times means about 1,000 turns a second.
- Put long agent runs on a durable queue. A web request then starts a run and receives the answer by streaming or polling, and workers take runs from the queue. Teams already works this way. Google Chat and web chat move to the same queue.
- Share the queue fairly between organisations. Each organisation gets its own share of workers, so one busy organisation cannot hold every worker.
- Set per organisation rate limits and model quotas with scoped rate limits, which already live in the database and apply across replicas.
- Configure a fallback chain for each agent's model, and buy enough provider quota for the peak. At this volume the provider quota, not the platform, is usually the limit.
- Autoscale app replicas and queue workers on queue depth and on the time a request waits for a worker thread.
- Keep the audit chain from becoming a single line. Every audited action takes one database wide lock. Write audit rows in batches, or keep one chain per organisation.
- Add a PostgreSQL read replica for console reads and reports, and run Qdrant as a cluster.
One instance for Teams, Google Chat and web chat
Picture three kinds of organisation on one instance. One uses Microsoft Teams, one uses Google Chat, and the others use the web chat. Today they share the same 40 worker threads, the same transcript list and the same CPU core.
- Teams messages go to a durable inbox and are answered 4 at a time across the whole instance. Teams retries a message it did not see acknowledged, and the inbox answers it once.
- Google Chat events start a new background thread each. They are not queued, so a burst can open many model calls and database connections at once, and an event in flight when the process restarts is lost. Events arrive from Google's network, so they share the rate limit of that address.
- Web chat runs inside the request, on the shared worker threads, and is limited per visitor address.
Until the steps above are done, size such an instance with the long running figure and leave room for the busiest organisation's peak. After the queue and the per organisation shares are in place, each organisation's channel draws on its own share of workers and its own rate limits, and a burst in one channel no longer slows the others.