SaaS architecture

SaaS noisy neighbor: what 6 queue simulations reveal

A bulk import can leave other customers waiting even when their own workloads are small. In our SaaS noisy neighbor simulation, tenant round robin reduced their p95 queue wait from 4.9 seconds to 0.2 seconds with the same 8 workers. A hard concurrency cap achieved the same wait but delayed the bulk workload, exposing a trade-off that a queue-depth chart alone would hide.

Experiment
6 deterministic simulations; synthetic workload
Capacity
8 workers serving 20 tenants
Population per run
400 bulk jobs and 76 quiet-tenant jobs
Policies
FIFO (first in, first out), tenant round robin and capped round robin
Conceptual model of busy amber and quieter blue job lanes entering shared processing slots.
Conceptual illustration of tenant workloads competing for shared processing capacity.

The same capacity can produce different waits

A noisy neighbor is a tenant whose use of shared resources degrades another tenant's experience. In background processing, the symptom may be an export that stays pending while another customer imports a large file. The affected customer did nothing unusual. Its jobs simply entered a queue behind work that already occupied the workers.

Microsoft frames the noisy neighbor problem as resource governance. That framing matters here: storing tenant data separately would not change which background task receives the next worker slot. This experiment isolates that scheduling decision.

Quiet-tenant p95 wait across 3 policies: 4.9, 0.2 and 0.2 seconds for equal jobs; 24.9, 0.5 and 0.2 seconds for slow bulk jobs.
Figure 1. Simulated queue waiting time, excluding execution. Lower is better. The two panels use different linear scales; exact values are listed below. View full-size figure.
Quiet-tenant waiting time across all 6 simulation runs
Bulk job durationPolicyp95 queue waitMaximum queue wait
100 msFIFO4,900 ms5,000 ms
100 msTenant round robin200 ms200 ms
100 msRound robin, cap 2200 ms200 ms
500 msFIFO24,900 ms25,000 ms
500 msTenant round robin500 ms500 ms
500 msRound robin, cap 2200 ms300 ms

Every policy completed the same jobs.

The differences came from when those jobs started. In the first workload, all jobs needed the same execution time. In the second, only the bulk tenant's jobs became slower. Round robin still helped the quiet tenants, but its p95 wait rose to 500 ms because running jobs could not be interrupted.

These are results from a deterministic model, not measurements of a deployed service. They support a narrow conclusion: with this arrival pattern and these execution times, changing admission order redistributed delay. They do not predict the latency of your application.

How the experiment was run

We wrote a discrete-event simulator that advances an integer clock to the next arrival or completion. A worker executes one job at a time. There is no sleeping, network traffic or random sampling. Each policy receives an identical copy of the workload, making the scheduling rule the only change within each scenario.

Method

Workers
8 identical slots
Tenant population
1 bulk tenant and 19 quiet tenants
Bulk arrival
400 jobs at simulated time 0
Quiet arrivals
Each quiet tenant sends 1 job at 200, 600, 1,000 and 1,400 ms
Service times
Quiet jobs: 100 ms; bulk jobs: 100 ms or 500 ms
Percentile rule
Nearest rank: the 73rd sorted waiting time out of 76 quiet jobs
Tie handling
Completions first, arrivals second, dispatch third; simultaneous arrivals use ascending job ID
Exclusions
None; all 476 jobs must complete

The quiet cohort is a fixed label in the input. It is not inferred from outcomes, and no inconvenient tenant is removed from the percentile. Queue wait means start time minus arrival time. Completion latency also includes service time. For these quiet jobs, it is 100 ms longer. Comparing one metric with the other would change the meaning of the result.

The dispatch rules tested in the simulator
PolicyNext job selectionTreatment of unused capacity
FIFOTake the oldest arrived job from one global queue.Dispatch whenever any job is waiting.
Tenant round robinRotate through tenant IDs and take one waiting job from the next nonempty tenant queue.Skip empty queues; a lone active tenant can fill all 8 slots.
Capped round robinUse the same rotation, but skip tenants with 2 jobs already running.The cap is hard. Slots stay idle when no tenant is eligible.

Results and verification

Each run writes a start and finish time for every job. An independent sweep over those intervals checks worker capacity and the per-tenant cap. Small fixtures with manually calculable answers check FIFO order, round-robin order, idle periods and the percentile rule. The script repeats every run and requires identical output. All 476 jobs completed in each run.

Limits

  • The model excludes broker delivery, polling and scheduling overhead. It cannot compare message-queue products.
  • All tasks succeed. Retries, crashes, database locks and connection limits are absent.
  • The input contains one finite burst. Sustained overload and several competing bulk tenants need separate experiments.
  • Queues are unbounded and workers never preempt a running job. Neither assumption should silently become a production requirement.

The workload was designed to expose a scheduling trade-off. It is not a random sample of SaaS traffic. Treat the output as a testable example, then replace its assumptions with measurements from the system you operate.

Put the tenant boundary before the worker pool

A scheduling policy needs a trustworthy answer to one question: whose work is this? Attach the tenant identity when the application accepts the operation, and carry it through the job record. Derive that identity from authenticated application context. Accepting an arbitrary tenant label from the payload would let callers choose which budget they consume.

Tenant context flows through admission limits into logical tenant queues, a dispatcher, 8 shared workers and downstream resource budgets.
Figure 2. A proposed application design. Logical tenant queues can share a broker. Fair dispatch governs worker admission; downstream limits govern the resources used after dispatch. View full-size figure.
  1. Admission: identify the tenant and work class, then decide whether to accept, defer or reject the operation.
  2. Logical queues: preserve the tenant identity while jobs wait. Separate logical queues do not require separate deployed brokers.
  3. Dispatch: choose an eligible tenant, reserve a worker slot and record the start time.
  4. Execution: apply separate budgets to database connections and external calls, then record the outcome and release reservations.

A shared dispatcher can coordinate separate tenant queues, or a broker can expose a fairness feature. The application still needs to know the feature's actual semantics. Amazon SQS fair queues, for example, use MessageGroupId as a tenant identifier on standard queues and prioritize other tenants when one has disproportionately many in-flight messages. AWS states that this does not enforce message ordering or impose a per-tenant consumption-rate limit.

Our round-robin algorithm does not reproduce that managed service. The distinction affects design: a fairness feature is not automatically a hard concurrency cap, and a tenant label is not automatically an ordering guarantee. Verify both behaviors before relying on them.

For an integration-heavy product, trace the full path from accepted API request to completed background operation. Dreamtsoft's REST API integrations overview is a separate entry point for its integration scope. The architecture illustrated here is a general design example.

A hard cap protects capacity by leaving some of it unused

The quiet-tenant chart makes the cap look attractive. The bulk completion chart shows the other half of the decision. A tenant with a large legitimate import also has a user waiting for the result. Record that user's completion target alongside the targets for the smaller jobs.

Bulk completion times for FIFO, round robin and capped round robin: 5, 6 and 20.8 seconds for equal jobs; 25, 26 and 100.2 seconds for slow bulk jobs.
Figure 3. Time from the bulk arrival until its last job completes. Lower is better. A hard cap cannot borrow idle worker slots. Panel scales differ. View full-size figure.
Bulk completion and total worker utilization over each complete run
ScenarioPolicyBulk finished atWorker utilization
Equal jobsFIFO5.0 s99.17%
Equal jobsTenant round robin6.0 s99.17%
Equal jobsRound robin, cap 220.8 s28.61%
Slow bulk jobsFIFO25.0 s99.81%
Slow bulk jobsTenant round robin26.0 s99.81%
Slow bulk jobsRound robin, cap 2100.2 s25.90%

Utilization here is total simulated service time divided by 8 times the interval from time 0 until the last completion. It describes occupied worker slots, not CPU usage or infrastructure cost. A worker waiting on a real database might occupy a slot while consuming little CPU.

Uncapped round robin finished the entire workload at the same time as FIFO in both scenarios: 6.0 seconds for equal jobs and 26.0 seconds for slow bulk jobs. It moved some bulk work later so quiet jobs could start earlier.

The result does not show a throughput increase. It shows a different allocation of a fixed capacity.

After the quiet jobs drain, the hard cap still permits only 2 bulk jobs to run. The other slots cannot help. A policy that lends unused capacity could avoid some of that delay, but would need a rule for taking the capacity back when another tenant arrives. That lending policy was not tested here.

Round robin distributes dispatch opportunities by job count. When one job occupies a worker much longer than another, equal turns do not imply equal resource time. Google's Handling Overload explains why request counts can be poor proxies for resource cost. For mixed workloads, consider separate work classes or measured resource budgets before assuming a count-based policy provides the desired isolation.

Choose the isolation boundary that matches the bottleneck

A slow queue can be the first visible symptom of a bottleneck elsewhere. If each worker opens a database connection and holds it for the whole job, admitting tenants fairly still leaves them competing for that connection pool. Adding queues would change organization without changing the scarce resource.

Candidate controls to evaluate against the observed failure
Observed conditionControl to evaluateWhat remains shared
Short tasks wait behind a bulk backlog.Tenant-aware dispatch across the shared worker pool.Workers and every downstream dependency.
A tenant occupies many slots with long tasks.Per-tenant concurrency cap or a separate work class.Storage capacity and external-service limits.
A tenant exhausts database connections.A connection budget at the database access boundary.Query execution resources and database locks.
Workloads require separate failure domains.Dedicated worker pools or deployment partitions.Any dependency that was left common.
Many tenants exceed total available capacity together.Admission limits and capacity planning against aggregate demand.The physical capacity ceiling until it changes.

Microsoft's multitenant messaging guidance describes shared, dedicated and hybrid arrangements. It also notes that dedicated messaging can leave other shared components exposed. Use that distinction when evaluating a platform as a service: identify what the provider isolates and what your application continues to share.

Security isolation needs its own review. A separate logical queue may help a scheduler recognize tenants without preventing a worker from reading another tenant's data. Authorization, credentials and data-access rules must remain effective independently of scheduling. Keep those checks in the job execution path, including retries and administrative replay.

Business priority is another input. A short job is not necessarily important, and a long job is not necessarily optional. Define work classes using the operation's consequence and deadline. A foreground confirmation and a scheduled bulk export can legitimately need different admission rules even when they belong to the same customer.

Before documenting a queue policy for a workflow automation use case, specify what happens when the relevant limit is reached. A caller needs a distinguishable outcome: accepted for later processing, rejected with a retry instruction or completed. An indefinitely pending task gives the user no reliable next action.

Test both the waiting customer and the bulk customer

Start with a trace you can explain. A global average can conceal the exact cohort that needs protection, while a quiet-tenant percentile can conceal a bulk workload that has stopped making acceptable progress. Compare both views for the same interval and keep the input trace fixed between policies.

  1. Record arrival, start and finish timestamps together with the tenant ID, work class and outcome. Keep queue waiting time separate from service time.
  2. Replay the same trace with the proposed policy and the current policy. Keep worker count and downstream budgets unchanged for this comparison.
  3. Inspect each tenant's waiting-time distribution and completion rate. Keep rejected, timed-out and failed jobs in the accounting instead of calculating latency only for successful work.
  4. Check the bulk workload's completion deadline. A policy that protects quiet jobs by making a legitimate import impractically slow needs a different budget or work class.
  5. Repeat with long jobs, coincident bursts and a downstream slowdown. Then interrupt a worker to test whether its reservations expire and work can resume.

Choose pass criteria before running the test. The 200 ms result in this model is not a sensible default target for every SaaS application. A target should describe a user-visible requirement, the traffic conditions under which it applies and the response when those conditions are exceeded.

Signals that distinguish scheduling delay from execution trouble
SignalWhat it helps diagnose
Queue wait by tenant and work classWhich accepted work waits for admission.
Service time and downstream waitWhether execution or a dependency became slower.
In-flight jobs per tenantWhether reservations enforce the intended cap.
Bulk completion timeWhether large workloads still finish acceptably.
Rejected and expired jobsWhether apparent latency improved by dropping work.
Worker occupancy alongside dependency usageWhether slots are busy doing useful work or waiting elsewhere.

Retries deserve a separate workload. Give replayed work the same tenant identity and an explicit attempt budget. Otherwise an unhealthy integration can return repeatedly as apparently new work. Google's overload guidance discusses client-side throttling because even rejected requests consume resources. Increasing rejection traffic without slowing senders can create another bottleneck.

For a rollout, retain a way to compare the new scheduler with the previous behavior and to revert its configuration. Treat the dispatcher as production code with its own availability requirements. A lost reservation or a tenant queue omitted from rotation can strand work even when every worker is healthy.

Reproduce the numbers, then change the assumptions

The experiment uses only the Python standard library. Download the simulation source, save it as queue_simulation.py and run it locally. The file is served as text so you can inspect it before execution. It creates a summary and a complete job trace in the output directory you choose.

Run the reference simulation without network access or additional packages.
python3 queue_simulation.py --out results

The reference results JSON records every assumption and all 6 summaries. The job trace CSV contains 2,856 rows, one per job per run. For one scenario and policy, select the 76 rows whose tenant is not 0, sort wait_ms and read the 73rd value. That independently reconstructs the reported p95.

For bulk completion, select tenant 0 and take the largest finish_ms. To reconstruct worker utilization, sum service_ms across all jobs in that run, then divide by 8 multiplied by the largest finish_ms. Multiply by 100 for a percentage. Preserve scenario and policy filters throughout. Combining runs would produce a meaningless denominator.

Change one assumption at a time. Begin with the bulk burst size, then its job duration. Move a quiet arrival inside a long-running batch to see the effect of nonpreemption. After those checks, add a second bulk tenant or vary service times using a trace from your application. Keep any customer identifiers and payloads out of shared experiment files.

A production candidate also needs behavior that this reference model deliberately omits: bounded backlog, durable job state, reservation expiry and clear failure outcomes. Write those requirements beside the policy before implementation. The useful deliverable is a dispatch rule with a reproducible workload and explicit limits on what its results establish.

Sources

Documentation checked .

  1. Microsoft: Noisy Neighbor antipattern
  2. Microsoft: Architectural approaches for messaging in multitenant solutions
  3. AWS: Amazon SQS fair queues
  4. Google SRE: Handling Overload