Skip to content

SaaS scaling problems at 10,000 users: the 6 that break first

Web · November 8, 2026 · 8 min read

A technical drawing: a wide field of small grey dots on the left with about twenty of them hatched white, their thin lines gathering into one line that branches to six hatched slabs stacked on the right, each slab split in two at a different point with its halves pulled apart

Ten thousand registered users is a small load. The arithmetic below puts it at around twenty requests in flight during the busiest hour. What breaks at that size are decisions that cost nothing at 500 users: a database connection opened on every request, a table read without an index, an email sent while the user waits. These are the SaaS scaling problems that show up first, and every one of them is cheaper to fix before the traffic arrives than after.

Why does 10,000 users break anything at all?

Because 10,000 users is not 10,000 people at once. Little's Law, a result from queueing theory, says the requests in flight equal the arrival rate times the time each request takes. Run the numbers for a B2B product. Assume 10% of users are active in the busiest hour, each one triggering a request every 10 seconds. That is 1,000 people and 100 requests per second, and at 200 milliseconds per request, 20 requests in flight. One modest server handles that.

So the load is small. The failures come from how each request spends what the system has: connections, rows scanned, seconds of work, calls to other services. A design that spends a little too much per request works at 500 users and stops at 10,000. Here are the six places it happens.

1. The database runs out of connections

PostgreSQL ships with max_connections set to 100 by default, and each connection is a server process with memory of its own (PostgreSQL docs). Managed plans set their own ceiling. On Supabase, the Micro compute allows 60 direct connections and 200 through its pooler, and the pooler limit only moves with a bigger compute (Supabase).

On serverless hosting, every function instance running at the same moment can open its own connection. A traffic spike, a deploy that cold-starts every instance, or a script inserting rows in a loop is enough to take all of them. The symptom is a wall of too many connections errors while the CPU sits idle.

The fix: route application traffic through a pooler in transaction mode, keep direct connections for migrations and long sessions, and cap the pool each instance may hold. Supabase's connection management guide recommends the same split. We covered when the pooler helps and when it does not in Supabase Supavisor: when the pooler saves you.

2. Queries that were fast at 1,000 rows

A query without an index reads the whole table. At 1,000 rows nobody notices. At 10,000 users, each writing events, invoices and comments, the same tables hold millions of rows, and a page that opened instantly starts taking seconds.

Two cases are worth checking first. The first is foreign keys. PostgreSQL indexes the referenced column, but by design it does not create an index on the referencing columns. Every project_id, tenant_id and user_id column needs its own index, written in the migration. The second is pagination with OFFSET. The documentation states that rows skipped by an OFFSET still have to be computed inside the server, so page 500 costs 500 pages of work. Keyset pagination (WHERE created_at < $last) costs the same on every page.

Row-level security adds a third case: a policy that calls a function per row turns one query into thousands of calls. The patterns that keep it fast are in Supabase RLS at scale.

3. Slow work runs inside the request

Sending the welcome email, rendering a PDF invoice, calling an AI model, posting to a webhook: each takes from half a second to half a minute, and each depends on a service the product does not control. Inside the request, every second of that work holds a function instance, often a database connection too, and the user waits for it.

When the outside service slows down, requests pile up behind it. Little's Law again: double the time per request and the requests in flight double, with no extra users.

The fix is a queue. The request records the job and returns in milliseconds. A worker does the work, retries on failure and stores the result. Two rules make it safe. Every job is idempotent, so a retry never sends two invoices. Every job has a timeout and a retry limit, so one bad input does not loop forever. A Redis-backed queue is one way to build it; the patterns we use are in the 5 Upstash Redis patterns.

4. One tenant slows down everyone else

In a multi-tenant product all customers share the same database and the same workers. Microsoft's architecture guidance calls the result the noisy neighbor problem: one tenant's activity degrades the service for the others, who see failed or slow requests. At 10,000 users the gap between the smallest and the largest account is wide enough for it to happen. One customer imports 200,000 rows on a Monday morning and the dashboard slows down for everyone.

The fix is to make the tenant a unit that can be measured and limited. Tag every query, job and log line with the tenant id. Give heavy operations (imports, exports, bulk edits) a per-tenant queue with its own concurrency, and set quotas by plan. When one account outgrows the shared setup, move it to its own database or its own workers. That move is far easier when tenancy was in the schema from the start, the argument of multi-tenant from day one.

5. Nothing limits what one client can ask for

OWASP ranks unrestricted resource consumption fourth in its 2023 API Security Top 10, and names the limits a vulnerable API is missing: execution timeouts, memory, records per page, and spending limits on third-party providers. A product with 10,000 users has scripts, integrations and the occasional abusive account. An endpoint that returns every record, or calls a paid AI model with no cap per user, turns one of them into an outage or an invoice.

The fix: rate limits per user and per API key, a maximum page size enforced on the server, a timeout on every outbound call, and a spending cap or billing alert on every paid provider. Each one is a few lines of code.

6. Background jobs that scan everything

The nightly job that recalculates usage, the report that totals revenue, the cleanup that deletes expired sessions: written at launch, each one reads the whole table. At 10,000 users the nightly run takes hours, overlaps with the next one, and holds locks during the morning's first traffic.

The fix is to process only what changed. Keep a watermark (the last processed id or timestamp), work in batches of a few thousand rows with a commit after each, and send heavy reads to a read replica where the plan has one. Update counters as events happen instead of recounting from zero every night.

How do you see these problems before the users do?

All six show up in numbers before they show up in support tickets. Four are enough to watch:

  • Open connections against the limit, with an alert at 70%.
  • The slowest queries by total time, from pg_stat_statements, reviewed every week.
  • p95 latency per endpoint, not the average, which hides the slow tail.
  • Queue depth and job age, so a stuck worker shows up within minutes.

Then test at the size the product expects to reach. Seed a staging database with ten times today's data, replay the busiest endpoints with a load tool such as k6, and read the query plans with EXPLAIN ANALYZE. An afternoon of this shows which of the six the product has.

What to fix first

Order by the cost of the failure, not by elegance. Connection limits and missing indexes take the whole product down and can be fixed in hours, so they come first. Queues and per-tenant limits take days and protect the product from its largest customers. Batching the nightly jobs can wait until they run longer than an hour. None of the six needs microservices, Kubernetes or a rewrite. One Postgres database and a well-built application carry a SaaS far past 10,000 users, as long as each request spends only what it needs.

Sources

Frequently asked questions

Rather have it done for you?

We design and build this kind of work for clients. Tell us what you need.