0%
Big Data Engines: Spark, Lakehouse, and Streaming at Scale
Distributed Execution
Spark
Query Engines
Streaming at Scale
Pipelines in Production
Scale Out Economics
Distributed data processing is a response to a constraint, and the constraint is more specific than people remember. It is not that data is large. It is that the time to process it on the largest machine you can rent has become longer than the time you are willing to wait, or that the data no longer fits in that machine's memory and the spilling has made it slower than the network would be.
That distinction matters because the first question in any big data design is whether you have reached the constraint at all. A surprising amount of work that runs on a twenty node cluster would run faster on one large machine, and would certainly run cheaper. Modern single instances offer hundreds of gigabytes of memory and dozens of cores for a few dollars an hour, and a well written single process program on one of them routinely beats a distributed job on a dataset of a hundred gigabytes, because it pays none of the coordination costs that make the distributed version possible.
What a cluster actually buys
A cluster buys three things and charges for all of them. It buys aggregate memory, so a working set larger than one machine can hold is spread across many. It buys aggregate disk bandwidth, which for scan heavy work is often the real limit rather than CPU. It buys parallelism, so a job whose work divides cleanly finishes in a fraction of the wall clock time.
What it charges is coordination. Every distributed job has to divide the work, move intermediate data between machines, handle machines that fail or run slow, and reassemble a result. Those costs do not shrink as you add nodes. Several of them grow, which produces the central fact of this subject: scaling out improves throughput up to a point and then stops improving it, and the point arrives earlier for some workloads than for others.
The data size that justifies a cluster
The honest thresholds have moved a great deal and most teams are working from numbers that were true a decade ago. Under about ten gigabytes, a single process on a laptop with a columnar library is usually the fastest thing available and nothing else is worth discussing. Between ten and a few hundred gigabytes, one large cloud instance with enough memory is almost always cheaper and faster than a cluster, and the engineering is simpler by an order of magnitude.
Past a terabyte of data that has to be scanned regularly, or past the point where a single machine's memory cannot hold the working set of the heaviest step, a distributed engine starts to earn its cost. Those boundaries are soft, and the quantity that decides them is not the size of the stored dataset but the size of the data each query or job actually touches, which columnar formats and partitioning can reduce by one or two orders of magnitude. A hundred terabyte table that is always queried for one day and three columns is a few gigabytes of real work.
The hidden input is the intermediate, not the source
Teams size clusters from the input table and are then surprised by what the job does in the middle. A join between two tables materialises pairs, and a poorly chosen join key can produce an intermediate several times the size of either side. A sort or a global aggregation needs memory proportional to the data being ordered rather than to the answer. A window function over an unbounded partition holds everything in that partition at once. Each of these can turn a comfortable job into one that spills to disk and runs five times slower.
The practical consequence is that the number worth measuring is the peak bytes held by the heaviest operation, which the query plan and the execution metrics will tell you if anybody looks. A job whose input is eighty gigabytes and whose peak intermediate is four hundred is a different decision from one whose input is eighty and whose peak is six, and the two are indistinguishable from the table size alone.
Vertical scaling is not finished
The industry narrative that vertical scaling hit a wall is out of date. Instances with twenty four terabytes of memory exist. Machines with hundreds of cores and local NVMe arrays reading at tens of gigabytes a second exist. For many workloads that were distributed in the twenty tens out of necessity, the necessity is gone, and the distributed version persists because the code was written and nobody has revisited the decision.
The reason to take this seriously is that a single machine has no shuffle, no stragglers, no partial failures and no partitioning strategy. It has none of the complexity that makes distributed jobs hard to debug, and it needs none of the people who know how to debug them. When vertical scaling fits, it is not merely cheaper in machine hours. It is dramatically cheaper in the engineering time that is usually the larger number.
Where scale out wins decisively
There are workloads where distribution is not a choice. Datasets that genuinely exceed the largest machine, which at the hundreds of terabytes scale is most of them. Work that must be done while tolerating machine failure, which at a thousand machine hours per job becomes a certainty rather than a risk. Jobs whose wall clock time has a business deadline that no single machine can meet regardless of cost. Multi tenant platforms where many teams share capacity, since a cluster is the mechanism by which that sharing happens at all.
The last one is underrated as a justification. Most organisations do not run a cluster because one job needs it. They run one because forty teams need occasional large capacity, and buying each of them a large machine would cost far more than a shared pool that is busy most of the time. That is a utilisation argument rather than a technical one, and it is often the real reason the platform exists.
The costs that do not appear in the instance bill
Comparing a cluster with a single machine on hourly rates understates the difference, because a distributed system brings expenses that arrive on other lines. Data transfer between availability zones, which a shuffle generates in volume and which is billed per gigabyte. Storage for intermediate data and for the logs and event files every engine writes. The control plane or management fee of a hosted service. The capacity that sits idle between jobs unless somebody has built autoscaling, and the autoscaling itself, which takes minutes to add workers and therefore has to be configured with a floor.
Then there is the engineering. Somebody has to tune partition counts, chase a straggler that only appears on Tuesdays, work out why a job that ran for a year suddenly spills, upgrade the engine, and answer a page when the cluster will not start. None of this appears when two instance types are compared in a browser tab, and in most organisations it is the dominant term. A fair comparison counts it, even approximately, because an approximate number on the right side of the ledger beats a precise number on the wrong one.
Decide it once, with numbers
The practical advice is to make this an explicit, written decision at the start of a project rather than an inherited default. Measure the actual data volume touched by the heaviest query. Price a single large instance that could hold it. Estimate the engineering cost of the distributed version honestly, including the operational load and the people who will be on call for it. Then choose, and record what would have to change for the choice to be revisited.
Teams that skip this arrive at a cluster by momentum and never leave it, because once the pipeline exists, moving it is a project nobody has time for. The decision is cheap at the beginning and expensive later, which is exactly the shape of decision that benefits from ten minutes of arithmetic.
Adding machines to a job does not divide its runtime by the number of machines, and the gap between that expectation and reality is the most important piece of arithmetic in distributed data processing. Understanding the shape of the curve lets you predict where the money stops buying speed, which is usually several nodes before anybody notices.
Two effects govern it. The first is that some part of every job does not parallelise, and that part sets a floor on the runtime no matter how wide the cluster. The second is that the machines have to talk to each other, and the cost of talking grows with the number of participants, which means a sufficiently large cluster can be slower than a smaller one.
The serial fraction sets the floor
If nine tenths of a job's work divides perfectly and one tenth does not, then infinite machines deliver a speedup of ten and no more. That is Amdahl's observation, and the uncomfortable part is how small a serial fraction has to be before large clusters make sense. At five percent serial the ceiling is twenty times. At one percent it is a hundred. Most real jobs have serial parts that are easy to overlook: reading a manifest, planning a query, listing files in object storage, writing a single output file, committing a transaction at the end.
The practical move is to find the serial part and measure it rather than estimate it. In most engines the job timeline shows it plainly as the period where one task is running and the rest of the cluster is idle, and that period is frequently dominated by something unglamorous like listing a partitioned directory with tens of thousands of files.
Coordination makes the curve turn down
Amdahl assumes the non parallel part is merely inert. In practice the machines interact, and that interaction has a cost that rises with cluster size. Coordination traffic, lock contention on shared metadata, scheduler overhead and the shuffle fan out all grow as nodes are added. The universal scalability law captures this with a term that is quadratic in node count, and its visible consequence is that throughput peaks and then declines.
This is why the question is not how much parallelism is available but where the peak sits for this job on this engine with this data layout. A job that peaks at sixteen nodes gains nothing from thirty two and may lose. The only reliable way to find the peak is to measure at several sizes, which takes an afternoon and saves a quarter of a cluster.
Strong scaling and weak scaling answer different questions
Strong scaling asks how much faster a fixed amount of work finishes as the cluster grows, and it is what people mean when they say a job does not scale. Weak scaling asks whether a cluster that grows along with the data keeps the runtime constant, and it is the property that actually matters for most data platforms, because the data grows every year and the deadline does not move.
A system can be poor at one and good at the other. An engine with a fixed per job planning cost scales weakly very well while scaling strongly quite badly, since the fixed cost is amortised over more work as both sides grow. Being clear about which question you are asking prevents a great deal of pointless argument about whether something scales.
Stragglers set the tail
A distributed stage finishes when its slowest task finishes, so the runtime is governed by the tail rather than the average. One task that takes four times as long as its peers because it received a skewed partition, landed on a slow disk or lost its node will dominate the stage. With a thousand tasks, the probability that at least one of them is unlucky approaches certainty.
Engines mitigate this with speculative execution, which launches a duplicate of a slow task and takes whichever finishes first. It helps with unlucky hardware and does nothing for skew, since a duplicate of a task with ten times the data is also slow. Knowing which of the two you have is the difference between a configuration change and a partitioning change, and the task duration distribution for the stage tells you immediately: hardware stragglers are a few outliers in an otherwise tight distribution, while skew is a long right tail with a shape.
Cost per job is a different curve from time per job
Runtime and money do not peak in the same place. Doubling a cluster that is still in its linear region leaves cost per job roughly flat, because twice the machines for half the time is the same machine hours. Past the knee that stops being true: the job takes more machine hours to do the same work, so cost rises while time barely falls. Plotting both curves on the same axis makes the trade visible and ends most arguments about sizing, because the region where time improves and cost holds steady is narrow and obvious once drawn.
There is a second effect that pushes the cost curve the other way. Short jobs on large clusters pay more of their time in startup, acquisition and planning, all of which are charged at the full cluster width. A job that runs for three minutes on sixty four nodes may spend a third of that on work that is not the job, which does not show up in the runtime comparison at all.
Measure speedup honestly
Benchmarks in this area are routinely wrong in predictable ways. A cold run includes cache population that a warm run does not. A job run against a small sample scales differently from the full dataset because the fixed costs dominate. A comparison that gives one side a tuned configuration and the other the defaults tells you about the tuning. Comparing a distributed engine against a single machine using the distributed engine in local mode compares the engine against itself with the coordination still in place, which is a common and misleading mistake.
A fair test runs each option at realistic data volume, three times, reports the median, uses each option's reasonable configuration, counts total cost rather than only time, and states what was excluded. That is not a research standard, it is the minimum required for the result to mean anything a month later.
Elasticity changes the question from size to shape
On a fixed cluster the only question is how many nodes to buy. On an elastic one the question becomes what shape of capacity the job should be given over its lifetime, and the curve is still the tool, applied per stage rather than per job. A pipeline whose first stage reads thousands of files and whose second stage joins two large tables has two different ideal widths, and an engine that can request and release workers between stages can use both.
The complication is that acquiring workers is not instant. Requesting capacity, starting containers and attaching storage take time that is charged against the stage that waited for it, and a job that scales up for a stage lasting ninety seconds may spend more time waiting than the stage saves. The rule that follows is to match elasticity to stage length: long stages are worth scaling for, short ones are not, and the cost of a wrong guess is visible in the gap between requested and running workers on the job timeline.
What the curve implies for cluster sizing
Once the curve is known, sizing follows from the deadline rather than from a feeling. If the job must finish in forty minutes and the curve says twelve nodes achieve forty five while sixteen achieve thirty eight, the answer is sixteen, and the fact that thirty two would also achieve thirty seven is the argument against thirty two. Running past the knee buys a rounding error of time at double the price.
The same curve is the tool for arguing about deadlines. A request to halve a runtime that is already near the flat part of the curve cannot be met by adding machines at any price, and showing the curve converts that from an engineering excuse into a visible property of the job. The useful next question then becomes which stage dominates and whether its work can be reduced rather than divided.
The phrase overhead hides a list of specific, measurable expenses, and engineers who can name them debug faster than engineers who treat the cluster as a black box that is sometimes slow. Every one of the costs below exists because the work is spread over machines that do not share memory, and every one of them has a different remedy.
Moving data between machines
The dominant cost in most distributed jobs is the shuffle, which is the step where data is redistributed so that all records with the same key end up on the same machine. A group by, a join on a non partitioned key, a sort and a distinct all require one. The shuffle writes intermediate data to disk on the sending side, transfers it over the network, and reads it on the receiving side, so a single shuffle touches disk twice and the network once for the whole dataset.
The number that matters is how that cost scales. With m map tasks and r reduce tasks there are up to m times r transfers, and while the bytes moved depend on the data rather than the task counts, the number of connections, requests and files does not. This is why a job with twenty thousand partitions on both sides can spend most of its time on request overhead while moving a modest amount of data.
Serialisation and the format tax
Before data crosses a network it has to be converted into bytes, and after it arrives it has to be converted back. For a Java or Python engine this can be a surprisingly large fraction of CPU time, particularly with generic serialisation that records type information for every record. Engines mitigate this with compact binary formats, columnar in memory layouts and code generation that emits specialised serialisers, which is why the same logical job can differ by several times in speed depending on how its data is represented.
Compression trades CPU for network and disk. A fast codec that compresses shuffle data by half usually pays for itself because network and disk are the scarcer resource, and a heavy codec that compresses by seventy percent often does not. The choice depends on the ratio of CPU to bandwidth on the machine type, which changes between instance families and is therefore worth checking rather than inheriting.
Fault tolerance is not free
Over a long job on many machines, failures are ordinary rather than exceptional, and tolerating them requires writing intermediate state somewhere durable enough to resume from. Checkpointing, replicating shuffle output and keeping lineage so lost partitions can be recomputed are all insurance premiums paid on every run, including the overwhelming majority where nothing fails.
The premium is adjustable. A short job on a small cluster can reasonably carry less of it and simply rerun on failure, while a six hour job on a thousand machines cannot, because the expected number of failures during the run is greater than one and a restart from the beginning would never finish. Choosing the level deliberately, rather than taking the default, is one of the few tuning decisions that changes the shape of the job rather than its constants.
Scheduling, startup and the empty cluster
Before any work happens, something has to acquire machines, start processes, distribute the job code and its dependencies, and assign tasks. On a long job these are rounding errors. On a job that runs for ninety seconds they can be most of the elapsed time, which is why interactive and small batch workloads often perform better on a warm pool or a long lived service than on a cluster created per job.
Task scheduling has its own cost that scales with task count. A job split into two hundred thousand tasks pays scheduler overhead two hundred thousand times, and the driver that coordinates them can become the bottleneck while every worker sits idle. This is the usual cause of a job that gets slower when the partition count is raised, and it is why partition counts have a sensible middle range rather than a direction.
Locality mattered, then stopped, then started again
The first generation of distributed processing was built around moving computation to the data, because a disk attached to the machine was far faster than the network to another rack. That assumption held when networks ran at one gigabit and local disks were the only fast storage available, and it is why early systems went to such lengths to schedule a task on a machine that already held its block.
Cloud architectures broke the assumption by separating storage from compute. With a network running at twenty five gigabits or more and object storage that any machine can read at similar speed, locality in the old sense is largely irrelevant, and the flexibility of scaling compute independently of storage is worth far more than the lost disk affinity.
What came back is a different form of locality: caching. Reading the same data repeatedly from object storage costs both time and request charges, so engines keep hot data on local NVMe and schedule tasks near the cache that holds it. The lesson is that locality is not a principle but a consequence of whatever the current speed ratios are, and those ratios have changed twice already.
Metadata and the cost of many small files
On object storage, listing is an API call, not a directory read. A table with two hundred thousand small files requires tens of thousands of list requests before a byte of data is processed, and those requests are serial enough to show up as a long period where the cluster is idle and one process is working. The remedy is file sizing and table formats that keep a manifest, and the symptom is distinctive once you have seen it: a job whose runtime depends on the number of files rather than their total size.
Small files also defeat the read optimisations that make columnar formats fast. A row group spread over a tiny file gives the reader no opportunity to skip, and the fixed cost of opening, reading the footer and decoding the schema is paid per file rather than per gigabyte. Compaction is unglamorous and routinely the single largest speedup available in a neglected lakehouse.
The driver is a single machine in a distributed system
Most engines have one coordinating process that plans the query, tracks every task, collects results and runs any code that is not expressed as a distributed operation. It is a single point of both failure and contention, and jobs frequently hit its limits long before the workers are busy. Collecting a large result to the coordinator, running a loop that issues thousands of small jobs, or planning a query over a table with hundreds of thousands of partitions are all ways to saturate one machine while the rest of the cluster waits.
The diagnostic is straightforward and underused: when a job is slow, check whether the workers are actually working. A timeline showing idle executors and a busy coordinator means the problem is not capacity, and adding nodes will make the contention slightly worse rather than better.
Skew turns an average into a tail
Partitioned work is only parallel if the partitions are similar in size. Real keys are not uniform: a handful of customers generate a third of the events, a null key collects every unmatched row, and a date partition for the day of an outage holds ten times the usual volume. One oversized partition becomes one oversized task, and that task becomes the stage's runtime.
This is the cost that most resembles a design error rather than a tax, because it is usually fixable. Salting a hot key, splitting a skewed join, broadcasting a small side instead of shuffling both and letting the engine's adaptive execution resize partitions all address it. What they have in common is that they change how the work is divided, which is why identifying skew correctly matters more than any configuration change.
A data platform's bill is rarely dominated by the thing people assume. Teams tune compute because compute is what they interact with, while the invoice is led by storage of data nobody queries, by transfer charges generated as a side effect of a shuffle, or by a warehouse running a dashboard refresh every five minutes that three people look at weekly.
The only way to argue about this productively is to decompose the bill, attribute it to workloads and express it per unit of work. A total is an argument nobody can act on. A cost per thousand queries, per terabyte processed or per pipeline run is a number an owner can be given.
The shape of a typical bill
Across most platforms the components fall into a recognisable set: compute for scheduled pipelines, compute for interactive and ad hoc queries, object storage for the data itself, intermediate and shuffle storage, data transfer between zones and regions, managed service or control plane fees, and licences for anything commercial in the stack.
The proportions differ from the intuition in two directions. Storage is usually cheaper than expected per terabyte, so a team can hoard data for years without the line looking alarming, while the compute that repeatedly scans that data is expensive and grows with every new consumer. Transfer is almost always larger than anyone guesses, because a shuffle across availability zones is billed per gigabyte in both directions and nothing in the job's own metrics mentions it.
The cheapest byte is the one never read
Processing cost is driven by bytes touched, so the largest savings in this field come from reading less rather than from reading faster. The reductions stack multiplicatively and each one is a different mechanism, which is why a well laid out table can cost a hundredth of a badly laid out one holding the same data.
Partition pruning removes whole directories or files before anything is opened, when the query filters on the partition column. Column pruning reads only the columns referenced, which in a wide table is often a tenth of the bytes. Row group statistics let a reader skip blocks whose minimum and maximum cannot match the predicate, which rewards sorting or clustering by the columns people filter on. Finally, pre aggregation replaces the scan entirely for queries whose answer was computed once and stored.
Storage is cheap until it is queried
A terabyte of object storage costs a few tens of dollars a month, which is why retention policies are the easiest thing in a platform to postpone. The expense arrives indirectly. Every additional year of history widens the tables that queries scan when a filter is missing, lengthens the file listings that precede every job, enlarges the backups and the replication to another region, and grows the metadata that the catalogue has to track.
The useful discipline is to separate data that is queried from data that is kept. Hot data stays in the format and layout that queries expect. Cold data moves to an archival tier, or is compacted into coarser aggregates that answer the questions anybody still asks of it. Deleting is also permitted, and the fact that it feels irreversible is the reason teams prefer to pay indefinitely for data they have never read.
Intermediate data is a line item too
Jobs leave behind more than their outputs. Shuffle files, checkpoints, spill, staging directories from failed writes, event logs and the per run artefacts of every orchestration tool all consume storage, and each individually looks too small to matter. On a platform running thousands of jobs a day they are not small, and because nothing owns them they survive indefinitely unless somebody writes a cleanup.
The symptom is a storage line that grows faster than the data does, with the growth concentrated in prefixes nobody recognises. The fix is unexciting: lifecycle rules on temporary prefixes, cleanup on job failure rather than only on success, and a quarterly look at what the largest prefixes actually are, which is nearly always a surprise the first time it is done.
Idle capacity and the shape of demand
Data platform demand is spiky by nature. A nightly batch window is busy for three hours and quiet for twenty one. Analyst queries cluster in office hours in one or two time zones. A cluster sized for the peak and left running is paying for capacity that does nothing most of the day, and that idle fraction is frequently the largest single waste on the platform.
The remedies are autoscaling with a sensible floor and a scale down policy that is actually allowed to act, separating workloads so that an interactive pool is not sized by a batch peak, and moving flexible work into the quiet window deliberately rather than leaving everything at nine in the morning because that is when somebody set the schedule. The last one costs nothing and is routinely worth a quarter of the compute bill.
Interruptible capacity and when it fits
Spot or preemptible instances are typically sixty to ninety percent cheaper than on demand, in exchange for being reclaimed with little warning. That trade fits work that is restartable, has slack in its deadline, and keeps no irreplaceable state on the worker. It fits badly for a coordinator, for a shuffle service whose loss destroys a stage's intermediate data, and for anything with a hard delivery time and no room to retry.
The practical pattern is a mix rather than a choice: stable capacity for the driver and any stateful service, interruptible capacity for the bulk of workers, and a fallback that converts to on demand if reclamation rates spike. Teams that adopt it wholesale and then suffer a run of reclaimed nodes usually conclude the mechanism is unreliable, when what failed was applying it to the parts that cannot tolerate it.
Separating storage from compute changed the economics
In the first generation of clusters, storage and compute were the same machines, so keeping more data meant running more nodes and the two costs were locked together. Separating them removed that constraint, and the consequences are easy to underrate. Capacity can now be sized for the work rather than for the data, several engines can read the same tables without copies, and a cluster can be destroyed at the end of a job without losing anything.
The cost is that every read crosses a network and is billed per request as well as per byte, which is what makes file sizing, caching and pruning matter so much more than they did when the disk was local. The architecture is better, but it moves the performance problem from how much hardware you own to how carefully the data is laid out, and a team that has not noticed the shift will keep solving the wrong half.
Billing models change the optimisation
Per second compute billing rewards finishing quickly and shutting down. Per byte scanned billing rewards reading less and is indifferent to how long a query takes. Reserved or committed capacity rewards keeping utilisation high, since the money is spent whether or not the cluster is busy, and turns idle time rather than runtime into the thing to attack.
The mistake is to carry an optimisation habit across a model boundary. On a committed cluster, making a job twice as fast saves nothing unless the freed capacity is used for something else, while on a per byte warehouse the same change may halve the line directly. Knowing which model you are under decides whether the next week of tuning work is worth anything at all.
Everything so far is input to one decision: what should run this workload, at what size, on what billing model. The decision is not difficult once the numbers exist. What goes wrong is that it is made implicitly, by whoever set up the first pipeline, and then inherited for years by people who assume it was reasoned about.
The remedy is a written decision with its evidence and its expiry conditions. That sounds bureaucratic and takes about an hour. It is the difference between a platform that is the right shape for its workload and one that is the right shape for a workload that existed in a previous role at a previous company.
Start from the workload, not the engine
The questions that decide the answer are all about the work. How much data does the heaviest step touch after pruning. What is the peak memory of its largest operation. How often does it run and what deadline does it have. Does it need to tolerate failure within a single run. How many concurrent users or jobs share the capacity. How variable is demand across the day and the week.
Answer those six and the shortlist is usually down to one or two options before any engine is named. Starting from the engine inverts the process and produces the familiar outcome where a tool is chosen for its reputation and the workload is then described in whatever terms make the choice look correct.
Benchmark your own workload, briefly
Published benchmarks tell you about the benchmark. They use data distributions, query shapes and concurrency patterns that resemble yours only by accident, and they are usually produced by a party with an interest in the result. A short internal test on a representative slice of your own data, running the queries you actually run, is worth more than any of them and takes a day.
A usable test uses realistic volume rather than a toy sample, runs each option with a configuration someone competent would choose, repeats enough to see variance, measures cost as well as time, and records what was not tested. Writing down the last part is what keeps the result honest when it is quoted six months later by someone who was not there.
Count the capability you have
A distributed platform requires people who can read a query plan, recognise skew, tune memory and be on call for it. That expertise is scarce and the market pays for it. A team of four generalists running one large instance and a scheduler will outperform the same team running a cluster they do not understand, and the difference will show up as incidents rather than as a line on the bill.
This is not an argument against distribution. It is an argument for counting it, and for choosing managed services more readily when the alternative is a small team maintaining infrastructure that needs a specialist. The honest version of the question is not whether a cluster would be faster but whether this team will still be able to operate it in eighteen months.
Platform decisions and project decisions are different
A team choosing how to run one pipeline is answering a narrow question and can afford to answer it locally. An organisation choosing what forty teams will run on is answering a different one, where the dominant terms are shared expertise, a single place to apply governance, consistent cost attribution and the ability to move people between teams without retraining them.
This is why the per workload answer and the platform answer legitimately diverge. A particular job might run perfectly well on a single machine while still belonging on the shared platform, because the marginal cost of running it there is near zero and the cost of it being the one thing nobody else knows how to operate is not. The honest framing is to say so plainly: the job does not need the cluster, and we are running it there because consistency is worth more than the instance hours.
The inverse case deserves the same honesty. A team with a genuinely different workload, such as one requiring very low latency or a specialised engine, should be allowed off the platform with a stated reason rather than forced to contort their problem to fit, since the contortion usually costs more than the exception.
Write the decision down with its triggers
A decision record for this needs very little: the workload numbers as measured, the options considered, the choice, the reasoning, and the conditions that would make it wrong. The last part is what gives the document a future. Conditions worth naming include data volume crossing a stated threshold, a deadline tightening past a measured capability, concurrency exceeding what was tested, the team losing or gaining the relevant expertise, and the bill crossing a number that justifies the migration effort.
With triggers written, revisiting becomes a scheduled check rather than an argument. Without them, the only thing that ever prompts a review is a crisis, and a crisis is the worst available moment to evaluate a migration.
Treat migration cost as part of the choice
Moving between engines is not a weekend. Pipelines have to be rewritten and verified to produce identical results, consumers have to be repointed, operational knowledge has to be rebuilt, and both systems usually run in parallel for a period while trust is established. That cost is why an adequate choice made early beats an optimal choice made after two migrations, and why the most valuable property of an early decision is reversibility rather than optimality.
Reversibility comes from specific habits: keeping data in open formats rather than proprietary internal ones, keeping transformation logic in portable dialects where practical, avoiding deep coupling to a single vendor's orchestration, and keeping the catalogue separate from the engine. None of these are free, and each one buys the ability to change an expensive decision later at a fraction of the usual price.
The failure mode is drift, not a wrong choice
Platforms rarely become expensive through one bad decision. They drift. A pipeline is added without an owner. A dashboard's refresh interval is lowered for a demonstration and never raised. A cluster is scaled up during an incident and never scaled back. Retention is extended because deleting felt risky. Each change is small, defensible and invisible on its own, and together they produce a bill that nobody can explain and a shape nobody designed.
Drift is countered by a small number of recurring checks rather than by a large review. Cost per workload published monthly, an owner required before a pipeline can be scheduled, an alert on unit cost rather than on the total, and a quarterly look at the largest storage prefixes will catch nearly all of it. The point is that these are routines, which drift cannot outrun, rather than projects, which it always will.
What good looks like after a year
A platform that has handled this well has a few recognisable properties. Every workload has an owner and a cost. The largest jobs have known scaling curves and are sized for their deadlines rather than for comfort. Data layout is maintained rather than inherited, with compaction and retention running on their own. Capacity is elastic where demand is spiky and committed where it is steady, and the two are not confused.
Those properties have a common root, which is that somebody can answer the question of what any given pipeline costs and why it is sized the way it is. A platform where that question has an answer tends to stay in good shape, because every subsequent decision is made against visible numbers rather than against an impression.
Most of all, the question of whether this should be distributed at all gets asked again when a workload changes shape, instead of being settled once in a meeting that nobody present remembers attending. That habit is cheap and compounds, which is the only reliable way to keep a platform from drifting into an expensive shape one reasonable decision at a time.