Multi-tenant SaaS: the mistakes that cost the most at your 1,000th customer
Table of contents
Most multi-tenant SaaS architecture problems don't show up at 20 customers. They show up at 300. That's when deploying a new version takes all night and one customer's data import slows everyone else down. On top of that, sales has promised an enterprise customer SSO that the permissions model can't handle. The decisions made at the MVP stage were sensible, because they let you ship the product fast. But some of them cost a week of work at 50 tenants and a whole quarter at 1,000.
Below I walk through nine such decisions. For each one I show what it looks like at the start and what breaks at scale. I also suggest what to do before a rewrite becomes the only option.
Data isolation in multi-tenant SaaS: decisions you can't cheaply undo
Mistake 1: an isolation model chosen by accident
At the start: all customers go into one database with a tenant_id column or, "because it's safer", each one gets its own database. At 10 customers both approaches work, so the choice comes from a tutorial rather than from analysis.
At 1,000 customers every model has its price:
- Shared database: the cheapest to maintain. But tables grow to hundreds of millions of rows, and a single large customer can slow down everyone's queries. Deleting a customer's data on a GDPR request is an operation across dozens of tables.
- Schema-per-tenant: gives better separation, but thousands of schemas put a strain on the database, backups and admin tools.
- Database-per-tenant: provides the strongest isolation and a simple restore for a single customer. The price is migration time: 30 seconds per database across 1,000 databases is over 8 hours. Backups, monitoring and the minimum instance cost are multiplied by a thousand.
What to do: for most products a hybrid model works best. Smaller customers stay on the shared database. Enterprise customers with contractual requirements (data residency, separate keys, separate backups) get a dedicated database, priced into their plan. There's one condition: the business logic doesn't know where the data lives, and a single layer of the system decides which database to use.
If you're just starting out, go with a shared database, add tenant_id to every table holding customer data and use UUIDs as identifiers. That way, moving a customer to a separate database later will be an export, not a project.
Mistake 2: isolation enforced on the honor system
At the start: every developer remembers to add a customer filter to their queries. With a few dozen screens, code review catches what's missing.
At 1,000 customers: there are hundreds of endpoints. Add background jobs, reports and exports written by new people on the team. One missed query is all it takes for customer A to see customer B's invoices. That's not an ordinary bug, it's a personal data breach. It has to be assessed and, if it poses a risk, reported to the supervisory authority (UODO in Poland) within 72 hours. With an enterprise customer, it also means a real risk of losing the contract.
What to do: isolation should be enforced by the system, not by team discipline. You need three independent layers:
- An automatic filter in the data access layer, e.g. a global scope in Laravel or a filter in Doctrine. When the tenant context is missing, the filter stops the operation.
- Row-level security in PostgreSQL, acting as a safety net for hand-written queries.
- Automated tests in CI. They create two tenants and check that one tenant's data never shows up for the other.
Ask your team how the tenant context gets into queued jobs. If you hear "we pass it manually", that's exactly where your first leak will happen.
The fail-closed principle: any place in the system that doesn't know which customer an operation relates to should abort it, not run it without a filter. A safeguard that simply switches itself off when context is missing is worse than none at all, because it gives a false sense of security.
Performance and deployments: noisy neighbors and migrations at a thousand tenants
This group of mistakes is usually the first to make itself felt. The symptom is distinctive: the SaaS can't keep up with growth, even though the servers aren't constantly overloaded. The spikes are intermittent and caused by a single customer.
Mistake 3: shared resources without limits (noisy neighbor)
At the start: there's one job queue, one cache and no API limits. At 10 customers there are more than enough resources.
At 1,000 customers: one customer imports 2 million records and clogs the queue. Password reset emails for everyone else wait 40 minutes. Another customer's integration hits the API dozens of times per second, and a report spanning five years of data ties up the database connections. Adding servers doesn't help, because the problem isn't a lack of capacity, it's a lack of boundaries.
What to do:
- Introduce per-tenant rate limiting based on the plan. It works as protection and as part of your offering at the same time.
- Split queues into critical work (login, payments), standard work and bulk work (imports, exports).
- Set a per-customer concurrency limit on the bulk queue so no one can take over all the workers.
- Move heavy reports to a database replica and define import size limits in your plans.
At the start this is a few hours of work. A year later it means reviewing every job in the system.
Mistake 4: deploying like a small monolith
At the start: the pipeline runs migrations and deploys code to all servers at once. With small tables, a schema change takes seconds.
At 1,000 customers: a change to a table with hundreds of millions of rows can lock it. Queries pile up and the application stops responding. With a database per customer the problem looks different: 1,000 schemas × 2 minutes is roughly 33 hours during which some customers are on the new schema and some on the old one. The business impact is that deployments happen at night, less often and with fear.
What to do: require migrations to follow the expand/contract pattern. First you add the new structures, then you migrate the data in the background, and only in the next release do you remove the old ones. The code must work with both the current and the previous schema version. Per-tenant feature flags decouple deployment from release. You turn a new feature on for yourself first, then for 5% of customers, and finally for everyone.
Configuration and identity: debt you can't pay off in installments
Mistake 5: "if it's customer X, do it differently"
At the start: your biggest customer wants a different invoice numbering scheme, so a condition on their ID appears in the code. Then come their logo, their domain and an ERP integration. With three exceptions, the whole team knows where they came from.
At 1,000 customers: there are hundreds of exceptions and nobody knows which ones are still active. Every change risks breaking something for a customer with a custom contract, so refactoring grinds to a halt and product development slows down.
What to do: the code shouldn't know any customer by name. Behavior comes from configuration, and configuration is data. In practice this means:
- typed tenant settings;
- pluggable strategies, e.g. for invoice numbering, approval workflows or document templates;
- integrations as separate connectors outside the application core;
- custom domains with automatic certificates.
Customization then becomes a product you can sell, not technical debt.
Mistake 6: a user belongs to a single tenant
At the start: an account is tied to one company, has one role and logs in with email and password. At 10 customers this matches reality.
At 1,000 customers reality drifts apart:
- an accountant handles 15 companies and has 15 accounts under different email addresses;
- an implementation partner needs access to many customers;
- an enterprise customer requires SSO (SAML or OIDC) plus automatic account provisioning and deprovisioning via SCIM;
- someone needs to be an admin in one company and just a viewer in another.
Changing this model touches every permission and every user-related screen. It's one of the most expensive refactors in SaaS, and it often blocks the signing of a big contract.
What to do right away: design a global user identity with tenant memberships, with a role assigned separately in each tenant. Build SSO on a proven identity provider, e.g. Keycloak, Auth0, Cognito or Entra ID. Home-grown SAML implementations are a classic source of security vulnerabilities.
Multi-tenant architecture doesn't break suddenly. It breaks quietly, in every place where the system assumes it knows the answer to the question: whose data is this, and what is this customer allowed to do?
Operations and money: costs, tenant lifecycle and billing
Mistake 7: you don't know what each customer costs
At the start: logs carry no customer information, metrics are global and there's a single cloud bill. At 10 customers that's enough, because you know everyone by name.
At 1,000 customers: response times go up and you don't know whether everyone is affected or just one tenant. Support can't find the logs for a ticket. You also don't know whether a customer on the cheapest plan is generating 30% of your AI model costs. As a result, you set pricing by gut feeling.
What to do: add the customer ID to every log and trace. Attribute direct costs, such as LLM tokens, text messages, paid APIs and storage, precisely. Allocate shared costs (servers, database) proportionally, e.g. by processing time. The goal is a monthly breakdown of cost and revenue for every tenant.
Mistake 8: manual onboarding and offboarding
At the start: a developer sets up each new customer by running a script. Data of customers who leave stays in the system forever.
At 1,000 customers: dozens of customers sign up via self-service every week, and an interrupted sign-up leaves orphaned resources behind. A GDPR deletion request requires cleaning up the database, files, the search engine and external systems. When a customer accidentally deletes their data, restoring it from a full-database backup takes a day of manual work.
What to do:
- Automate provisioning as a repeatable process with states (active, suspended, pending deletion).
- Keep a register of every place where customer data lives.
- Build data export as a product feature.
- Delete data in two stages, with a grace period.
- Plan your backups: either a clear retention period in the contract or a separate encryption key per tenant, whose deletion makes the data in backups unreadable.
Mistake 9: billing and limits bolted on at the end
At the start: the subscription lives in Stripe and the plan name in the database. Limits are checked in a few places, or not at all.
At 1,000 customers: sales wants user limits, usage-based billing, add-ons and enterprise contracts. Meanwhile, an "if plan is Pro" condition is scattered across dozens of places in the code, and there's no usage history. Changing the pricing turns into a months-long project.
What to do: separate three concepts:
- plan: the commercial product;
- entitlements: what the tenant can do, i.e. features and limits;
- metering: what the tenant has used.
The code only ever asks about entitlements, never about the plan name. Record usage as an immutable event log. That same log feeds billing and the per-customer cost breakdown.
If you can only do two things this quarter, add the customer ID to your logs and start recording usage events. Both changes are cheap and don't require restructuring your data. In return, they give you the information you need for every decision that follows: on limits, pricing and investments in isolation.
Checklist before your 100th customer
If you're planning to build a SaaS platform or already have a few dozen customers, treat this list as the bare minimum. Every item is cheap today and gets expensive once you pass a few hundred tenants.
- Every table with customer data has a tenant_id, and identifiers are UUIDs.
- A single layer of the system decides which database to use, and the business logic doesn't know where the data lives.
- Isolation has three layers: an ORM filter, row-level security and leak tests in CI.
- Missing tenant context stops the operation, including in background jobs.
- Rate limiting works per tenant, and limits come from plan entitlements.
- Queues are separated (critical, standard, bulk) and have a per-customer concurrency limit.
- Migrations follow expand/contract, and new features are turned on with per-tenant feature flags.
- There are no customer IDs in the code, and customization is configuration.
- The user model is based on memberships with a per-tenant role and has a ready plan for SSO and SCIM.
- Logs include the customer ID, and direct costs are attributed to tenants.
- You have a documented tenant lifecycle: provisioning, export, GDPR-compliant deletion and single-customer restore.
Summary: multi-tenant SaaS is a series of deferred decisions
None of these mistakes stems from incompetence. They're sensible MVP-stage shortcuts whose cost grows along with your customer count. Most of them can be fixed incrementally, without rewriting the system, provided you start at 100 customers rather than 1,000.
A multi-tenant SaaS that scales well shares a few traits. Isolation is enforced by the infrastructure, resources have limits, and configuration and permissions are data. The cost of every customer is known.
At Midapt we design these foundations as part of SaaS product development and put them in order in existing systems during modernization. If you feel your MVP architecture is starting to creak, book a free consultation. We'll go through the checklist together and show you where to start.
Frequently asked questions about multi-tenant SaaS
Yes, as long as isolation is enforced technically and not just through developer discipline. The minimum is an automatic filter in the data access layer that stops the operation when tenant context is missing, plus row-level security in the database and tests that check one tenant's data never shows up for another. A dedicated database is mainly needed for contractual or regulatory reasons.
Schema-per-tenant is cheaper in terms of infrastructure, but with hundreds of schemas it strains the database and slows down backups. Database-per-tenant gives the strongest isolation and easy single-customer restores, but it multiplies migrations, connections and costs. For most products, the best fit is a shared database for smaller customers and dedicated databases for enterprise, priced into the plan.
It's when one tenant consumes so many shared resources that it slows down everyone else. This can involve the job queue, database connections, cache or CPU. Typical causes are large imports, heavy API integrations and resource-intensive reports. The fixes are per-tenant rate limiting, separate queues for bulk work, concurrency limits and plan limits enforced by the system.
Measure direct costs, i.e. LLM tokens, text messages, paid APIs and storage, precisely by attributing every event to a customer. Allocate shared costs, such as servers and the database, proportionally using an approximate measure, e.g. processing time or data volume. Comparing cost with revenue per tenant shows which customers and plans are unprofitable.
Yes, but the cost depends on how heavily the code assumes a specific model. It's easiest when every table has a tenant_id, identifiers are UUIDs and database selection happens in a single layer. Then customers can be moved gradually, one at a time, with no downtime. Without these foundations, the change usually means a refactor lasting many months.


