A useful SaaS scaling plan starts with a workload, a tenant boundary and a failure you can recognize. Adding application instances helps only when the constrained resource can benefit. It will not repair an unscoped query, a congested database or a retry loop sending duplicate notifications.
Consider a hypothetical B2B client-approval application. Each agency has a workspace containing projects and deliverable revisions. Staff prepare revisions; invited clients review designated projects. An approval belongs to a particular revision, and a new revision needs a new decision. One person can belong to several workspaces. This guide addresses an engineering lead improving that existing workload; our SaaS development planning guide covers defining the product and pilot.
Write a workload and separate the targets
Record dataset size, tenant size distribution, peak arrival rate, operation mix and background work. “Ten thousand users” does not say whether they log in once a month or all upload files before the same deadline. Include the busiest tenant and the smaller tenants sharing its resources.
For a rehearsal, invent 200 workspaces containing 50,000 deliverable revisions, with the largest workspace holding 40% of them. Exercise 20 metadata reads and two approval submissions per second for 15 minutes, then a five-minute burst at twice those rates. These are illustrative test inputs, not capacity claims or recommended sizing.
| Concern | Example target | Evidence to collect |
|---|---|---|
| Response time | 95th-percentile metadata reads below 400 milliseconds during the steady workload. | Measure request-to-response latency at the test client; report failures separately and exclude file transfer explicitly. |
| Availability | 99.9% of eligible production operations succeed over a calendar month. | Define eligible operations and count timeouts and server errors as failures; valid permission denials are not outages. |
| Recovery | Restore service within two hours with at most 15 minutes of lost committed data. | Time a restore drill and reconcile revisions, approvals and file references against the stated recovery point. |
A short load test cannot prove monthly availability or a recovery objective. Record the client region, deployment size, database engine, cache state and concurrent background work so another engineer can repeat the experiment. Report tenant-specific outliers alongside the overall percentile.
Find the constrained stage before changing architecture
Trace one slow workflow through application processing, SQL, object storage and external calls. Compare a baseline with the same workload after one change. If project pages issue a query per revision, investigate that repeated access before adding workers. If exports wait while request latency stays stable, investigate queue scheduling rather than the web tier.
Our Laravel Pulse guide explains aggregate application measurements; the Horizon guide separates Redis queue behavior from business completion. Add database query plans and sanitized trace or log samples when a dashboard cannot explain one execution. Keep raw customer content out of diagnostic labels.
Serverless hosting still has limits. AWS documents Lambda concurrency and execution quotas; downstream database connections and provider throttling remain part of the design. Test the whole operation instead of treating automatic compute scaling as a promise of unlimited throughput or uninterrupted service.
Choose where tenants share resources
Authentication identifies a person; authorization decides what that person can do in the selected workspace. Data placement is another decision. Azure's multitenant storage guidance describes the trade-offs between shared resources and dedicated databases. For the approval application, compare the operational consequences:
| Approach | Useful property | Work to account for |
|---|---|---|
| Pooled tables | One schema rollout and a shared capacity pool. | Every access path needs tenant scoping; restoring only one agency's records requires a deliberate procedure. |
| Database per workspace | A separate data boundary and more direct tenant restore procedures. | Connection routing, provisioning, migration tracking and backup verification grow with the database fleet. |
| Hybrid placement | Move a demanding workspace to separate resources while others share. | Maintain a trustworthy workspace-to-resource map and rehearse movement without losing or duplicating writes. |
Separate databases can still share constrained compute. Shared application credentials can still select the wrong database. Choose isolation requirements for each layer, then test routing and access rules. Do not commit to a migration merely because one customer's dataset is large; first establish whether contention, recovery needs or an explicit customer requirement justifies it.
A narrow Laravel example: authorize membership and scope the read
The following Laravel 12 helper implements one read-only rule: active workspace owners, admins and members may read a deliverable's metadata within that workspace. External client reviewers are deliberately rejected here; they need a separate project-grant policy. Reading metadata does not authorize approving, deleting or downloading a file.
Assume projects(id, workspace_id), deliverables(id, project_id, revision, title) and workspace_memberships(workspace_id, user_id, role, revoked_at), with integer IDs, enforced parent references and one membership row per workspace/user pair. In this example each deliverable row represents a revision. Save the helper as app/Support/readStaffDeliverable.php:
<?php
use App\Models\User;
use Illuminate\Support\Facades\DB;
function readStaffDeliverable(
User $user,
int $workspaceId,
int $deliverableId,
): object {
$mayRead = DB::table('workspace_memberships')
->where('workspace_id', $workspaceId)
->where('user_id', $user->getAuthIdentifier())
->whereNull('revoked_at')
->whereIn('role', ['owner', 'admin', 'member'])
->exists();
abort_unless($mayRead, 403);
$deliverable = DB::table('deliverables')
->join('projects', 'projects.id', '=', 'deliverables.project_id')
->where('projects.workspace_id', $workspaceId)
->where('deliverables.id', $deliverableId)
->select('deliverables.id', 'deliverables.project_id',
'deliverables.revision', 'deliverables.title')
->first();
abort_if($deliverable === null, 404);
return $deliverable;
}
Call it from an authenticated controller with the server-authenticated $request->user() and validated integer workspace and deliverable IDs. Load the file with require_once app_path('Support/readStaffDeliverable.php') if it is not autoloaded. Never construct the user from a client-supplied user ID.
The membership query checks the selected workspace on each invocation. The record query reaches that workspace through the deliverable's project, so a valid record ID from another workspace returns 404. Missing, revoked or disallowed membership returns 403. In a larger application, organize action rules with Laravel policies; the query builder join makes this example's record scope visible.
We exercised the unchanged helper against real SQLite 3.45.1 queries using Laravel 12.40.2 and PHP 8.3.6: 15 cases and 62 assertions passed. Checks covered the three allowed staff roles, rejected reviewer and missing memberships, wrong-workspace record IDs, a user belonging to two workspaces, and membership revocation committed before the next call. The fixture inspected returned objects and framework abort exceptions, including their status codes. Only the selected metadata columns were returned. This verifies the tested read boundaries, not HTTP authentication, concurrent revocation, production database behavior or performance.
This is a bounded read example, not a complete tenancy framework. The two queries do not provide atomic protection against a revocation racing an in-flight request. Approval writes need their own authorization and concurrency design, including the revision being approved. Search, exports, file access, support tooling and background jobs each need equivalent boundary checks.
Make queries and caches respect the same boundary
For the project revision list, examine the actual filter, sort and pagination before proposing an index. A candidate index starting with project_id may help that access pattern; verify its plan on the production engine and a realistic dataset. Index maintenance adds write cost. The single-record helper above is not evidence that a bulk listing query is efficient.
A cache key such as workspace:42:project:8:revision:3:summary:v1 demonstrates explicit scope, not a complete permission check. A cached response restricted by user or role also needs that visibility boundary. Authorize before returning it, define invalidation when a revision or grant changes, and test access withdrawal with the cache already warm. Never allow a public CDN cache to turn a private project response into shared content.
Keep workspace context explicit in service calls. Mutable global “current tenant” state is particularly risky in long-running workers: the next job must not inherit the previous job's context. Test consecutive jobs for different workspaces in the same process.
Protect smaller tenants from heavy background work
Suppose one agency requests hundreds of exports while another is waiting for an approval notification. Azure's noisy-neighbor guidance treats this as resource governance. For this application, separate interactive notifications from bulk exports, bound export size and active work per workspace, and define what users see when they reach a limit.
A shared FIFO queue does not automatically deliver tenant fairness. Measure wait time and completion by workload class and tenant cohort; ensure small tenants still make progress under the heavy-tenant rehearsal. More consumers can worsen database contention, so set concurrency against the constrained downstream resource.
Carry workspace, project, revision and operation identifiers in job payloads. Re-resolve the records within that workspace when executing, and decide whether the job acts for a current user permission or a narrowly defined system operation. For a user-requested export, recheck access before generating and releasing it; a queued request is not permanent authorization.
Laravel's after-commit dispatch avoids a worker observing changes before their transaction commits. It does not make the database commit and broker delivery one atomic operation. Where losing an approval notification is unacceptable, consider a transactional outbox and reconciliation. Make repeated deliveries safe using the operation and revision identity.
Rehearse rollback and recovery as separate operations
A release rollback restores code; it does not undo approvals already written or external notifications already sent. Keep schema changes compatible with the prior application version during the rollback window. Expand the schema, move readers and writers, verify the migration, and remove obsolete fields in a later release.
Before rollout, record the conditions that stop it: increased denied-access anomalies, growing queue delay, elevated error rate or a tenant-specific regression. Assign an operator, preserve the release identifier and test restarting long-running workers with the intended version. A feature flag can stop new exports while existing work drains, but it must not bypass authorization.
Restore a backup into an isolated environment and verify project ownership, revision decisions and object references. For pooled data, practice recovering one workspace without overwriting other tenants' newer approvals. Measure the elapsed recovery and actual recoverable point; a successful backup command alone cannot establish either objective.
Turn the investigation into a bounded change: a baseline, a proposed fix, tenant-boundary checks, workload results and a recovery procedure. Our SaaS budget guide connects that scope to delivery cost, and our SaaS development service can help review the implementation and evidence.