Warehouse Workload Isolation and Concurrency

At 8:55 on the first working day of the month, a finance team’s dashboards in a hypothetical company stop loading. The queries are small and the tables are well laid out. Nothing is wrong with the SQL. What changed is that a scheduled export of the entire previous month started at 8:50 on the same warehouse, and its long scans occupy every execution slot. The dashboard queries are not slow; they are waiting. From the user’s side the symptom is identical to a slow query, but the fix is entirely different, and an engineer who cannot tell the two apart will resize the warehouse, change nothing about the waiting, and pay more.

This article is about that situation: many workloads sharing a pool of analytical compute. It covers how to separate compute so one workload cannot starve another, how queues and concurrency limits behave and why, how to define workload classes, how access controls keep the separation from being bypassed, and how to diagnose which of several causes is behind a slow query. It builds on Partitioning, Compaction, and Table Maintenance, which showed that a table’s file layout decides how much work each query does. Layout controls the size of the work, and this article is about who gets compute to do it and what happens when demand exceeds supply. Product behavior and defaults were read from the Snowflake, Google BigQuery, and Amazon Redshift documentation on October 3, 2026. The product mechanisms differ in vocabulary more than in the problems they address, so the article states each problem first and then maps it to each product.

Compute separation: what is isolated and what is still shared

A data warehouse that separates storage from compute lets several independent pools of compute read the same data. That is the structural basis for isolation: if the dashboard workload and the export workload run on different compute, the export’s scans do not occupy the dashboard’s slots. The workload isolation entry makes the point that isolation is not a single property. It can mean limiting resource contention, containing failures, restricting access, or attributing cost, and a boundary built for one rarely delivers the others. Keep that in mind when reading each product’s mechanism, because each separates compute and leaves other things shared.

Snowflake: virtual warehouses

A virtual warehouse is Snowflake’s unit of compute. The documentation says warehouses do not share compute resources with one another, so several can query the same tables simultaneously without taking each other’s compute. A warehouse has a size, and for standard Gen1 warehouses each step up doubles the compute and the credit rate. Resizing a running warehouse does not speed up queries already executing, because the added resources are used only for queued and newly submitted queries (Snowflake warehouse considerations). The documentation also states that resizing is not intended for handling concurrency issues and recommends additional warehouses or a multi-cluster warehouse for that.

Warehouses still interact through shared state. Two warehouses writing the same table can block each other with transaction locks, and the load-monitoring chart reports a distinct Blocked status for queries waiting on a lock, separate from Queued for warehouse overload (Snowflake, Monitoring warehouse load). Isolating compute does not isolate contention on the data itself.

BigQuery: reservations and assignments

BigQuery organizes compute as slots. A slot is the unit of compute, and under capacity pricing, slots are allocated in pools called reservations. The documentation’s example is separate prod and test reservations so that test jobs do not compete with production. A reservation is used by assigning it to projects, folders, or an organization, and the most specific assignment wins: a project assignment overrides a folder assignment, which overrides an organization assignment. A project with no reservation uses on-demand pricing. Assignments carry a job type, and the documented types include QUERY for non-continuous queries, PIPELINE for load and extract jobs, and several BACKGROUND_* types for background jobs such as change-data-capture ingestion and search-index refresh, so different job types in the same project can be routed to different reservations (BigQuery, Understand reservations).

Reservations are not hard walls. The documentation says idle slots can be shared across reservations, and a reservation with baseline slots has capacity that is always available and always billed, while autoscaling capacity is added under demand and is billed per second with a one-minute minimum by default. The same page warns that if a production workload relies on idle slots from other reservations during testing, performance can degrade when other reservations become busy, so time-bound production workloads should have a self-reliant reservation. That is the central trade-off of any pooled design: sharing improves utilization and weakens the guarantee.

Redshift: queues and concurrency scaling clusters

Amazon Redshift’s workload management (WLM) divides a cluster’s concurrency and memory among queues. Automatic WLM lets Redshift decide how many queries run concurrently and how much memory each receives, and it supports query priorities within queues. Manual WLM exposes queues, user groups, and query groups so an administrator routes queries by who runs them or by a label and sets memory per queue (Amazon Redshift, Workload management). Separately, concurrency scaling adds cluster capacity when demand rises: eligible queries from a WLM queue go to a concurrency-scaling cluster instead of waiting, for reads and for common writes such as COPY, INSERT, DELETE, and UPDATE, with charges only for the time those clusters actively run queries (Amazon Redshift, Concurrency scaling).

The three side by side

QuestionSnowflakeBigQueryRedshift
Unit of separate computeVirtual warehouseReservation (a pool of slots)WLM queue within a cluster; concurrency-scaling clusters for bursts
How a workload is placedThe session or role uses a named warehouseProject, folder, or organization assignment, by job typeUser group or query group routing (manual WLM); automatic WLM decides dynamically
How capacity grows for burstsResize, add warehouses, or multi-cluster auto-scaleAutoscaling slots above a baseline; idle slots from other reservationsConcurrency scaling clusters
What is still sharedLocks on shared tables; storage and metadata servicesIdle slots (by design); datasets and jobs in the same reservation competeCluster resources inside the main cluster, between queues

This table is a reading aid assembled from the cited documentation, not a ranking. The mechanisms are not interchangeable: a Snowflake warehouse is a separately billed unit of compute, a BigQuery reservation is an accounting and scheduling construct over a shared slot pool, and a Redshift queue partitions one cluster. Choosing between products on isolation grounds means asking what your required guarantee is (no queueing for a class, a bound on its queueing, or only a cost boundary) and which mechanism gives it.

Concurrency and queues

Why concurrency is a derived quantity

A common mistake is to treat the concurrency limit as a number of users. The concurrency and request rate entry defines request concurrency as requests started but not yet finished, which depends on the arrival rate and on how long each request stays in the system. If queries arrive at 0.5 per second and each runs for 2 seconds, about one is running at any time on average. If the same arrival rate meets a 20-second export, about ten are. This relation (average number in the system equals arrival rate times time in the system) is a standard queueing result known as Little’s law, and it is the reason a single slow workload can raise concurrency for everyone without any increase in users.

Queueing is nonlinear in utilization

When demand is below capacity, queries rarely wait. As utilization approaches 1, waiting grows sharply, and that growth is the main reason capacity planning works with headroom. The standard model for a pool of identical servers is the Erlang C formula, which gives the probability that an arriving query must wait, assuming Poisson arrivals, exponentially distributed service times, first-in-first-out service, and c identical slots. Real warehouses violate these assumptions, since query durations vary much more than exponential and engines allocate resources by query size, so use the model for the shape and not for forecasts. The script below computes the table for 8 slots and an assumed 20-second mean query.

import math

def erlang_c(c, a):
    """Probability an arriving query must wait, for c slots and offered load a = arrival_rate * service_time."""
    tail = a**c / math.factorial(c) * c / (c - a)
    return tail / (sum(a**k / math.factorial(k) for k in range(c)) + tail)

c, service = 8, 20.0                      # 8 slots, 20 s mean query time (assumed)
print("utilization  arrivals/min  P(wait)  mean wait (s)")
for rho in (0.50, 0.70, 0.85, 0.95):
    a = rho * c
    p = erlang_c(c, a)
    print(f"{rho:11.2f}  {a / service * 60:12.1f}  {p:7.3f}  {p * service / (c * (1 - rho)):13.2f}")
utilization  arrivals/min  P(wait)  mean wait (s)
       0.50          12.0    0.059           0.30
       0.70          16.8    0.271           2.26
       0.85          20.4    0.573           9.54
       0.95          22.8    0.844          42.21

Read the rows as a progression. At 50% utilization, 5.9% of queries wait and the mean wait is 0.3 seconds. Moving from 85% to 95% utilization, an increase of ten points, raises the mean wait from 9.5 to 42 seconds. The relationship between load and delay is convex, and it is steepest exactly where an operator is tempted to run hot to save money. A real workload with heavy-tailed query durations does worse than this model suggests, because one long query blocks a slot for many short ones.

How the products govern concurrency

Snowflake’s MAX_CONCURRENCY_LEVEL parameter, default 8, sets how many SQL statements a warehouse cluster executes concurrently. When the level is reached, statements queue in a single-cluster or maximized multi-cluster warehouse, and an auto-scale multi-cluster warehouse starts additional clusters instead. The documentation notes the level is not exact: small statements count as a fraction toward it, so more may run, and large complex statements mean fewer. Lowering the level gives each query more resources and can speed up large queries, while raising it may reduce the resources per statement without necessarily raising the total number running; in auto-scale mode, lowering the level can increase the number of active clusters (Snowflake parameters, MAX_CONCURRENCY_LEVEL). Two timeouts bound the damage of a backlog. STATEMENT_QUEUED_TIMEOUT_IN_SECONDS cancels a statement that has waited in the queue too long, with default 0, which means no timeout. STATEMENT_TIMEOUT_IN_SECONDS cancels a statement that runs too long, with default 172,800 seconds (2 days), and the documentation states it covers all of the time taken, including queue time, locked time, compilation, and execution. Both can be set on a warehouse, and when set at both session and warehouse levels the lowest non-zero value applies.

A multi-cluster warehouse runs more clusters of the same size. It addresses queueing, not slow individual queries, as the multi-cluster warehouse entry explains: the diagnostic is whether queries are slow while running or slow while waiting. Its Standard scaling policy starts a cluster when a query queues or when Snowflake estimates the running clusters cannot take more, and Economy waits until there is enough load to keep a new cluster busy for about six minutes, trading some queueing for fewer credits (Snowflake multi-cluster warehouses).

BigQuery computes concurrency dynamically per on-demand project or per reservation, queues the excess, and limits the queue to 1,000 interactive queries and 20,000 batch queries per project per region. For reservations, an optional maximum concurrency target (default 0, meaning dynamic) is an upper bound that guarantees each running query a minimum amount of slot capacity; raising it does not guarantee more queries run simultaneously, because the slots must exist. Scheduling is fair across projects: if a reservation serves projects A and B, and A has four queries running while B has one, a query from B is dequeued first even if submitted later. Batch queries have their own dynamic maximum, so when it is reached, interactive queries are prioritized. Queue timeouts are set with default_interactive_query_queue_timeout_ms and default_batch_query_queue_timeout_ms, and a timeout of -1 turns queueing off so queries beyond the concurrency limit fail with an ADMISSION_DENIED error (BigQuery, Use query queues). A separate preview feature, project caps, sets maximum slots and maximum concurrency per project within a reservation, applies the concurrency cap only at admission time, and does not cancel running queries when lowered; the page labels it pre-GA as of the date read (BigQuery, Understand reservations).

Redshift’s automatic WLM determines concurrency and memory per dispatched query. Manual WLM exposes it. The documentation recommends configuring manual queues with a total of 15 or fewer query slots and caps a queue at 50 slots, while noting that automatic WLM does not always run 50 queries at once because memory and other resources decide. Query monitoring rules attach metrics-based boundaries to a queue: a rule has up to three predicates and one action (log, hop, or abort), a queue can have up to 25 rules with a limit of 25 across all queues, and metrics are evaluated every 10 seconds (Amazon Redshift, Query monitoring rules). For example, a queue meant for short queries can cancel anything that runs longer than a bound you choose.

A queue is an admission-control decision

Whatever the product, a queue and its timeout decide who waits, for how long, and what happens at the limit. Three design questions follow. What should happen to a query that has waited past the point where its result is useful? A dashboard refresh that waits 5 minutes may be worthless, and canceling it frees capacity. Should excess demand queue, scale out, or be rejected? Rejection (BigQuery’s -1) is the right answer when callers can retry later and unbounded queues would hide overload. And how do callers retry? Clients that retry immediately on failure turn a brief overload into sustained overload, a pattern the backpressure entry addresses from the streaming side. Retries should back off, and a queue timeout should be shorter than the caller’s own timeout so work is not run for a caller that has already gone.

Workload classes

A workload class is a group of queries that share a latency expectation, a resource profile, and an owner. The point of defining classes is to give each a different policy. Classes should come from observed query history, not from team names. A reasonable starting list for an analytical warehouse follows, offered as a design pattern and not as a standard.

ClassTypical profileWhat mattersPolicy direction
Interactive BI and dashboardsMany short queries, bursty at predictable timesLow wait time and stable p95Protected capacity; short queue timeout; scale out for bursts
Scheduled transformations (ELT)Few large, long queries, often at nightFinishing before a deadlineOwn pool sized to the deadline; can queue; long timeout
Ad hoc explorationUnpredictable, occasionally enormousNot harming othersCapped pool; per-query limits; timeouts
Data science and training extractsLarge scans, long runningThroughputSeparate pool or batch priority
Table maintenanceCompaction and cleanup reading and rewriting whole partitionsCompletion, not latencyScheduled outside peak hours, isolated from the interactive pool
Exports and reverse ETLFull-table or large-range reads, sometimes overlapping othersPredictable durationOwn pool or lowest priority; timeout

Classification needs an identifier on every query. Roles, service accounts, warehouse or project assignment, and labels are the common ones. Any class that cannot be identified on arrival cannot be given a policy, so the first design task is making the class visible in the query record.

A model of three policies

The following simulation, written for this article with the standard library, compares three ways to run a hypothetical mixed workload on 8 slots. The workload is illustrative: dashboard queries arrive as a Poisson process at about 0.5 per second and run for about 2 seconds each (mean), and at 60 seconds twelve 300-second batch jobs arrive together. Each query takes one slot, service is first-in-first-out within the policy, and durations are fixed or exponentially distributed. These are assumptions, and real engines allocate resources by query demand instead of one slot per query. The three cases are a shared pool with no limits, a shared pool with the batch class capped at 4 concurrent slots, and separate pools of 3 and 5 slots.

"""Illustrative queue model: one-slot-per-query, FIFO admission, hypothetical workload."""
import heapq, random, statistics

def make_jobs(seed=7, horizon=1200):
    rnd = random.Random(seed)
    jobs, t = [], 0.0
    while t < horizon:                       # BI: Poisson arrivals, ~0.5 per second, ~2 s each
        t += rnd.expovariate(0.5)
        jobs.append(("bi", t, rnd.expovariate(1 / 2.0)))
    for i in range(12):                      # ETL: 12 heavy jobs of 300 s arrive together at t = 60
        jobs.append(("etl", 60.0 + i * 0.1, 300.0))
    return sorted(jobs, key=lambda j: j[1])

def simulate(jobs, slots, class_cap=None):
    """slots: total slots. class_cap: optional max concurrent slots per class."""
    class_cap = class_cap or {}
    running = []                             # heap of (finish_time, class)
    in_use = {"bi": 0, "etl": 0}
    waiting = []                             # FIFO of (arrival, class, service)
    waits = {"bi": [], "etl": []}
    finish = {"bi": 0.0, "etl": 0.0}
    pending = list(jobs)
    now = 0.0
    def admit():
        nonlocal waiting
        progressed = True
        while progressed and len(running) < slots:
            progressed = False
            for k, (arr, cls, svc) in enumerate(waiting):   # first eligible job in FIFO order
                if in_use[cls] < class_cap.get(cls, slots):
                    waiting.pop(k)
                    heapq.heappush(running, (now + svc, cls)); in_use[cls] += 1
                    waits[cls].append(now - arr); finish[cls] = max(finish[cls], now + svc)
                    progressed = True
                    break
    while pending or waiting or running:
        next_arr = pending[0][1] if pending else float("inf")
        next_fin = running[0][0] if running else float("inf")
        if next_arr <= next_fin:
            now = next_arr; cls, arr, svc = pending.pop(0); waiting.append((arr, cls, svc))
        else:
            now, cls = heapq.heappop(running); in_use[cls] -= 1
        admit()
    return waits, finish

def pct(xs, p):
    xs = sorted(xs); return xs[min(len(xs) - 1, int(p / 100 * len(xs)))]

jobs = make_jobs()
bi_jobs = [j for j in jobs if j[0] == "bi"]; etl_jobs = [j for j in jobs if j[0] == "etl"]
print("BI queries:", len(bi_jobs), " ETL jobs:", len(etl_jobs))
cases = {
    "A shared 8 slots, no limits":           lambda: simulate(jobs, 8),
    "B shared 8 slots, ETL capped at 4":     lambda: simulate(jobs, 8, {"etl": 4}),
    "C separate pools: BI 3 slots, ETL 5":   lambda: (
        (lambda a, b: ({"bi": a[0]["bi"], "etl": b[0]["etl"]}, {"bi": a[1]["bi"], "etl": b[1]["etl"]}))(
            simulate(bi_jobs, 3), simulate(etl_jobs, 5))),
}
print(f"{'case':40s} {'BI p50':>7s} {'BI p95':>8s} {'BI p99':>8s} {'BI max':>8s} {'ETL done (s)':>13s}")
for name, f in cases.items():
    w, fin = f()
    b = w["bi"]
    print(f"{name:40s} {pct(b,50):7.2f} {pct(b,95):8.2f} {pct(b,99):8.2f} {max(b):8.2f} {fin['etl']:13.1f}")
BI queries: 642  ETL jobs: 12
case                                      BI p50   BI p95   BI p99   BI max  ETL done (s)
A shared 8 slots, no limits                 0.00   266.34   293.64   297.74         660.3
B shared 8 slots, ETL capped at 4           0.00     0.00     0.47     1.71         960.3
C separate pools: BI 3 slots, ETL 5         0.00     0.97     2.39     2.93         960.1

In case A, eight of the twelve batch jobs take all eight slots at second 60 and hold them for 300 seconds, so dashboard queries arriving in that window wait up to nearly 300 seconds (p95 of 266 s). The batch work finishes at 660 seconds, which is the fastest of the three. Case B caps the batch class at four concurrent slots. Dashboard queries now have four slots always available, p99 wait falls to 0.47 seconds, and the batch work finishes at 960 seconds because it runs in three waves of four. Case C separates the pools into 3 and 5 slots, giving similar batch completion (960 s) and a slightly worse dashboard tail than B (p99 of 2.39 s) because the dashboard pool cannot borrow the batch pool’s slots.

The lesson is a trade between three quantities, and no case wins all of them. Isolation of the interactive class cost the batch class 45% more elapsed time here (660 to 960 seconds). Pooled capacity with a cap (B) kept more flexibility than fixed partitions (C), since dashboards could use up to all eight slots when batch work was absent, and batch jobs could use up to four. This matches the BigQuery design that lets reservations lend idle slots, and the warning that borrowed capacity is not a guarantee. The simulation does not model cost. In a real deployment, separate pools mean separately billed compute, each idle part of the day. Whether the batch class’s extra 300 seconds matters is a business question. If the batch job must finish before 9:00 and runs overnight, the cap costs nothing. If it feeds a 9:30 report, it might.

Access controls: making the separation hold

Compute separation fails if anyone can run anything anywhere, or can reconfigure any pool. Access control for workload isolation has two distinct halves: who may run queries on which compute, and who may change that compute’s configuration. They need different permissions.

Snowflake separates them at the privilege level. USAGE on a warehouse lets a role run queries on it. OPERATE lets a role stop, start, suspend, and resume it, view queries, and abort running queries. MODIFY lets a role change any property including size, and is required to assign the warehouse to a resource monitor, though only the ACCOUNTADMIN role can make that assignment. MONITOR lets a role view queries and usage statistics. The global MANAGE WAREHOUSES privilege is equivalent to MODIFY, MONITOR, and OPERATE on all warehouses (Snowflake, Access control privileges). A role with MODIFY on a warehouse can change its size, concurrency level, and timeouts, which means it can quietly remove the protection that the class design relies on. Grant MODIFY to the owner of a class, not to every user of the compute.

In BigQuery, the compute a job uses is determined by the reservation assigned to the project that runs the job, so permission to run jobs in a project is effectively permission to use that project’s assigned capacity. Creating reservations and assignments is a separate permission on the administration project that holds the commitments: the documentation names roles/bigquery.resourceAdmin and roles/bigquery.resourceEditor as providing the permission to create assignments and to set concurrency on a new reservation. Queries can also be told at runtime to use a specific reservation, overriding the default assignment. The reservation must be in the same region as the query, and the caller must have access to the reservation to use it (BigQuery, Understand reservations; BigQuery, Use query queues). Access to the reservation is therefore the control that decides who can point work at a protected pool.

In manual Redshift WLM, user groups and query groups route queries to queues, so the boundary is who belongs to which group and, where routing is by a label that the query specifies, who is able to specify that label. Check whether a user can pick their own queue before relying on label-based routing for a class that must not be self-selected; that behavior was not verified in the documentation read.

The general principles are those of least privilege applied to compute: separate the right to run from the right to reconfigure, give each class its own service identity so its queries are identifiable, and make a bypass path (a shared role that can use every pool) a deliberate exception. Data access is a different layer, covered by role-based and attribute-based access control. Having permission to use a pool says nothing about which tables a query may read, and the reverse is also true. An audit of one layer says nothing about the other.

Cost guardrails belong here too. A Snowflake resource monitor tracks credit usage against a quota and can notify or suspend assigned warehouses at percentage thresholds, but it cannot track serverless features, and a suspend action stops the class it protects as well as the class that overspent. Decide in advance which class is allowed to be stopped by a cost limit, and set the monitor on that class’s warehouse. The risk the monitor introduces is a dashboard warehouse suspended for the last week of the month because the ad hoc class used the shared quota.

Performance diagnosis: waiting or working?

When someone reports a slow query, the first question is whether time went to waiting or to running. These have different causes, and most wasted tuning comes from guessing the wrong one.

Separate queue time from execution time

Each platform exposes this split. Snowflake’s load-monitoring chart divides query load into running, queued for provisioning (typically in the first minutes after a warehouse resumes), blocked on a transaction lock, and queued for warehouse overload; the chart requires the MONITOR privilege on the warehouse (Snowflake, Monitoring warehouse load). In BigQuery, the documentation points to INFORMATION_SCHEMA.JOBS* views and the Jobs API for slot usage, and the slot entry notes that totalSlotMs in the query plan separates compute consumed from bytes processed (BigQuery, Understand reservations). The general approach is the same on any warehouse: record submission time, start time, and end time for every query, and tag each with its class.

ObservationLikely causeEvidence to gatherTypical response
Long wait before start; run time normalCapacity or concurrency exhaustedQueue time by class and hour; running queries at that moment; who submitted themIsolate the heavy class; scale out the protected class; cap the heavy class; add a queue timeout
Long wait only just after a warehouse resumedProvisioning delayThe provisioning status in the load chartKeep a minimum capacity for latency-sensitive classes; accept cold start for batch
Wait caused by a lock, not capacityConcurrent writers on the same tableBlocked status in the load chart; the transaction that holds the lockSerialize or schedule the writers; this is a data contention problem and more compute does not fix it
Run time long; wait shortScan volume, layout, skew, or spillFiles or bytes scanned versus available; stage-level skew; spillFix layout as in the maintenance article; address data skew; resize for the query
Fast the second timeResult cache or warm local cacheWhether the query text, role, and data are unchangedMeasure with cache state recorded; see the result cache entry
Slower after scaling downCache dropped with the removed resourcesResize history against latencyWeigh credit savings against the cache loss (Snowflake documents that decreasing size drops the associated cache)
Everyone slower at onceShared dependency or a noisy neighbor in a shared poolPer-class and per-tenant latency and resource useSeparate the neighbor; check for shared locks or services

The cache lines matter because they corrupt measurement. A query that is fast on its second run measured the cache, and a benchmark that discards the first run measures the best case. The honest comparisons use bytes scanned and queue time, which do not depend on cache state, along with wall time recorded with the cache state noted.

A hypothetical diagnosis

Illustrative scenario: dashboard p95 rises from 3 seconds to 40 seconds every day at 09:00. Splitting each query’s time into queue and run shows run time steady at about 2.5 seconds and queue time rising from near zero to a p95 of 37 seconds. That rules out layout and points to capacity. Listing the queries running at 09:00 shows three long exports submitted at 08:55 by a service account that shares a warehouse with the dashboards. The queue starts because exports hold slots. Resizing the warehouse would shorten the exports but would not stop them from occupying slots when the dashboards arrive, and the documentation says resizing is not a concurrency fix. The remedy is the one the simulation showed: put the export class on its own capacity, or cap it, and set a short queue timeout on the dashboard class so a backlog fails fast instead of piling up. The numbers in this scenario are invented for illustration. The method of splitting time before choosing a fix is the point.

The cost side of each fix

Every remedy has a cost. A larger Snowflake size doubles the credit rate per step but shortens runs; the virtual warehouse entry notes that the credits stay similar only if run time roughly halves, which is not guaranteed for small queries. Multi-cluster auto-scale spends credits per running cluster, and the maximum cluster count is the cap on that spending. BigQuery baseline slots cost whether or not they are used, and autoscaling slots cost only while scaled up. Redshift concurrency scaling clusters are charged for the time they actively run queries. Separate pools raise the idle cost of each. A diagnosis is complete when it names the fix and the cost the fix adds.

Validating a workload design

A design for isolation is a hypothesis about your workload, and the test is to compare the hypothesis with measurements. A workable sequence follows.

  1. Record per-class demand. For each class and hour, record arrival rate, the distribution of run time (not only the mean), and peak concurrency. The percentile entry’s warning applies: do not average per-warehouse p95 values into a combined one.
  2. Set a target per class. State a bound on queue time or total latency for each class, for example a p95 for dashboards and a completion time for batch. Without a target, no design can pass or fail.
  3. Choose headroom from the queueing curve. The Erlang table shows delay rising steeply at high utilization. A latency-sensitive class needs substantially lower planned utilization than a batch class that can queue. Pick a figure from your own measured tail, because an assumed distribution understates it.
  4. Replay before changing production. Replay a recorded busy period against the proposed configuration if the platform allows it, or apply the change to one class at a time and compare queue time and cost over comparable periods.
  5. Change one control at a time. Concurrency level, size, cluster count, and timeouts interact, so a combined change cannot be attributed.
  6. Watch for second-order effects. Capped or separated classes shift load: batch jobs finish later, retries rise, and a protected class may be starved of cache. Track batch completion time and failure counts as well as dashboard latency.

Do not infer a general ranking from this material. No benchmark was run for this article, the model assumes one slot per query, and the three products were read from documentation only, not exercised. What transfers is the method: separate waiting from running, identify the class of the queries involved, and choose the control that addresses that cause.

Failure modes

FailureMechanismMitigation
Dashboards stall behind a large jobA shared pool with no class limitsCap or isolate the heavy class; queue timeout on the interactive class
Resizing does not helpThe problem is queueing, not run time; resize affects only queued and new workDiagnose queue versus run time first; use scale-out or isolation
Retry storm during overloadClients retry immediately after failures or timeoutsBackoff with jitter; queue timeout shorter than the client’s own; reject early
Cost monitor suspends the wrong classShared quota and a suspend actionPut monitors on per-class warehouses; decide in advance who may be stopped
A role reconfigures a shared poolMODIFY-style rights granted too widelySeparate run and reconfigure rights; one owner per class
Borrowed capacity vanishesIdle slots shared across reservations are reclaimed when the owner needs themGive time-critical classes a self-reliant baseline
Lock waits misread as capacity problemsConcurrent writers contend on a tableCheck lock-wait status; schedule writers; more compute will not help
Pools sit idle most of the dayFixed partitions with uneven demandPrefer pooled capacity with caps, or schedule batch into idle windows

References


Discover more from Insightful Data Lab

Subscribe to get the latest posts sent to your email.

Similar Posts

Questions, corrections, or additional insights?

This site uses Akismet to reduce spam. Learn how your comment data is processed.