When the consumer at the end of a pipeline is a language model rather than a dashboard, the expensive step moves from the transform to the last hop, and every row you push through it costs money. This article covers the pipeline shape we keep returning to, a four-question test for choosing batch, event-driven or request-path inference, and the three cost levers that matter, in the order they matter.

Photo: Derrick Coetzee, “Front of server racks at NERSC”, Wikimedia Commons, CC0 1.0 public
domain dedication
Most teams adding an AI feature to an existing product start by asking which model to use, or whether they need a streaming platform. Both matter far less to the bill than a duller question.
How many model calls does one business event cause, and how many of them could have waited, or not happened at all?
A pipeline built around that question stays predictable. A pipeline that treats the model as one more sink, like a reporting table, tends to produce the invoice that gets the feature switched off.
The Consumer Changed, the Pipeline Did Not
A classic analytics pipeline ends in a dashboard. The transform is the heavy step, the output is an aggregate, and reprocessing a day of data is cheap because warehouse compute is cheap per row. Staleness of a few hours is usually fine.
A pipeline that feeds an AI feature inverts most of that:
- The expensive step is the last one. Warehouse SQL over a few hundred thousand rows costs little. Sending those same rows to a model provider is billed per token, per row.
- The output is per record, not aggregated. A product description, a ticket category, a summary for one report. There is no “roll it up” shortcut.
- Reprocessing is no longer free. A backfill that is harmless for a dashboard can be the most expensive job of the month when a model sits at the end of it.
- Idempotency becomes a cost control, not just a correctness property. A retry that re-sends a row is a second charge.
The practical consequence: design the pipeline so the model sees as few rows as possible, each as small as possible, and never the same unchanged row twice.
The Pipeline Shape We Keep Coming Back To
Across the AI retrofits we have shipped into products that were already in production, from ticket triage on a marketplace to drafting product copy and summarising reports for internal reviewers, the architecture has settled into four layers.
The operational store stays the write path, the warehouse prepares a model input contract, and a worker only calls the model for rows whose input actually changed.

Figure 1: The pipeline shape. The operational store stays the write path, the warehouse prepares a model input contract, and a worker only calls the model for rows whose input actually changed.
Our examples use BigQuery and a Node.js worker, because that is where most of this work runs. The shape maps directly onto AWS: object storage plus Athena or Redshift for the warehouse layer, and a scheduled container task for the worker.
Layer 1: Ingestion, Kept Deliberately Boring
The operational database stays the write path. The relevant tables are exported, by a managed streaming export or a scheduled one, into append-only raw tables in the warehouse.
This is the same additive pattern we use for any workload that outgrows the operational database: keep writes where they are and build the new read path elsewhere. The AI feature can then be removed without touching the product’s core data.
Layer 2: Transformation Produces a Model Input Contract
The most useful artifact in the whole pipeline is a single view that defines exactly what the model is allowed to see, in exactly the shape the prompt reads. Nothing else is sent.
-- One row per record the model may see, in the shape the prompt reads.
CREATE OR REPLACE VIEW ai.product_copy_input AS
SELECT
p.product_id,
p.title,
p.category,
p.origin,
p.unit,
-- Hash only the fields the prompt uses. Price and stock are excluded on purpose.
TO_HEX(SHA256(TO_JSON_STRING(STRUCT(p.title, p.category, p.origin, p.unit)))) AS input_hash
FROM raw.products_latest AS p
WHERE p.status = 'unpublished';
Three things happen in that view. Fields are trimmed to the handful the prompt needs. Anything that must not leave your infrastructure is removed or replaced with a placeholder here, in SQL you can review, not in application code scattered across services. And input_hash gives every row a content-based identity the next layer uses to skip work.
On a feature that read health-related customer records, replacing a full record with a hand-picked field set cut prompt tokens by more than half, and reduced what left our infrastructure at all. That one change beat every model-pricing decision on the same feature.
Layer 3: The Inference Worker Is a Pipeline Stage
The model call belongs in a worker that behaves like any other pipeline stage: it selects its inputs, processes them in batches, validates its outputs, and records what it did.
interface ModelInput { productId: string; inputHash: string; prompt: string }
async function runNightlyDrafts(cap: SpendCap): Promise<void> {
// Only rows whose input hash has never been sent to the model
const rows: ModelInput[] = await warehouse.query(`
SELECT i.* FROM ai.product_copy_input AS i
LEFT JOIN ai.processed_inputs AS d USING (product_id, input_hash)
WHERE d.product_id IS NULL`);
for (const batch of chunk(rows, 50)) {
if (!cap.allows(estimateTokens(batch))) break; // stop quietly, the app keeps its fallback
const results = await provider.generate(batch);
const valid = results.filter(isValidDraft); // schema check, invalid output is dropped
await sidecar.upsert(valid); // keyed by product_id and input_hash
await warehouse.insert('ai.processed_inputs', results.map(toLedgerRow)); // failures too, or they are re-sent nightly
cap.record(results);
}
}
The output never overwrites the product’s own fields. It lands in a sidecar table keyed by record ID and input hash, and the application reads it when a valid row exists.
When none exists, because the worker has not run, the cap was hit or validation failed, the application renders what it rendered before the feature existed. Removing the feature means deleting a table, not reversing a migration.
Batch, Event-Driven or Request Path: A Four-Question Test
Most “batch versus streaming” debates about AI features are really a question about who is waiting. We run every feature through four questions, in order, and stop at the first one that gives a clear answer.

Figure 2: The four-question test. Each question is an exit; most features leave at question one or two.
1. Can the Output Be Computed Before Anyone Asks for It?
If yes, it is a scheduled batch job, full stop. Latency is free, batch pricing from providers applies, and the input hash means the nightly run only touches rows that changed.
Product description drafting is our clearest example: retailers submit products during the day, drafts appear overnight in a draft field, and a person approves before anything is published.
2. Does a Person Wait on Screen for the Result?
If nobody is watching, it is event-triggered and asynchronous: a post-write trigger puts the record on a queue, a worker calls the model, and the result lands later.
Support ticket triage works this way. The ticket is saved first, the customer sees no added latency, and the suggested category appears for the ops team a few seconds later. If the call fails or exceeds its timeout, the ticket goes to the default queue as it always did.
For most AI features, this is what “streaming” means: a trigger and a queue. A dedicated event streaming platform earns its place when many independent consumers need the same event history, which one AI feature rarely justifies.
3. Did the Person Explicitly Ask for It?
If a user clicked “tidy up this description”, it is a user-triggered call. Seconds are acceptable because they asked and are watching a loading state. It still needs a cancel path and the original content preserved.
4. Can the Page Render Acceptably If the Call Is Skipped?
Only now do we consider the request path, and only with a hard timeout well under the page’s existing latency budget and a deterministic fallback that renders the pre-AI experience.
If the page cannot render without the model’s answer, the feature is not ready for that path. Precompute it, or do not ship it there.
Where the Bill Actually Comes From
The monthly cost of an AI feature is roughly:
calls per business event × tokens per call × event volume
The price per token is the factor people argue about, and it is the one we touch last.
Across our retrofits, per-call cost varied by two orders of magnitude between features, and the levers that moved it were, in order of impact:
- Trim the input. Covered in layer 2. Fewer fields, fewer tokens, less data leaving your systems.
- Key the cache on content, not identity. See below.
- Switch models, but only with an eval set. A smaller model is cheaper per call, but without a fixed set of real inputs and assertions to prove quality held, a model switch is a guess with a saving attached.
Key the Cache on Content, Not Identity
An expensive mistake we have made ourselves is deciding whether to regenerate based on the record: its ID plus an updated_at timestamp. Records change constantly for reasons the model does not care about.

Figure 3: An illustrative sequence of changes to one product. Keyed on identity, every change triggers a model call. Keyed on a hash of the fields the prompt reads, only the changes the model would notice do.
Keyed on identity, every change triggers a model call. Keyed on a hash of the fields the prompt reads, only the changes the model would notice do.
On a marketplace feature that generates copy once per product version and serves it thousands of times, moving the cache key from the product ID to a hash of the normalised attribute set meant regeneration only happened when attributes actually changed.
Monthly spend on that feature dropped to a fraction of its launch figure, with no change to the model or the prompt.
Put the Spend Cap in the Pipeline
Provider dashboards can alert you, but they cannot make your product degrade gracefully.
We write a hard ceiling into the worker itself, as in the snippet above. When it is reached, the worker stops and the application falls back, so an overspend becomes a quiet degradation someone reviews in the morning rather than an invoice discovered at month end.
Where Managed Services Stop Being Worth It
The pitch for a managed ETL connector, a distributed compute cluster, or a dedicated vector database is usually made as if the data volume were the hard part. For most AI features inside an existing product, it is not.
The volume that matters is bounded by business events: products listed, tickets opened, reports produced.
Our rules of thumb:
- Managed connectors earn their fee for SaaS sources you do not control and whose APIs change under you. For your own operational database, a native export into the warehouse is usually simpler and cheaper.
- Distributed compute such as Spark earns its place when the transform itself is the heavy step: preparing very large corpora, non-SQL processing at scale, or feeding self-hosted models. When the transform fits in warehouse SQL and the expensive step is a rate-limited API call, a cluster adds operational weight without shortening the part that is slow.
- Serverless functions suit short triggers. Long batch runs belong in a container runtime with no execution time ceiling.
- A dedicated vector store is worth it once the corpus and query volume outgrow what your existing database or warehouse can serve. For a staff-only internal search over a modest document set, it is often one more system to secure and keep in sync.
The trade-off we accept is a pipeline that looks unimpressive on an architecture slide. In return, fewer systems hold copies of the data, which matters when a deletion request has to reach every copy.
When a Pipeline Is the Wrong Answer
Not every AI feature needs one.
If the feature is user-triggered, operates on content already on screen, and runs a few times a day per user, a direct call with a timeout and a preserved original is simpler and cheaper than any pipeline.
A pipeline also cannot fix missing rules. We once scoped automatic routing of support messages against categories that existed in a dropdown, while the real routing logic lived in one person’s head and contradicted it.
No amount of data engineering helps there. If the rules cannot be written down before work starts, write them down first.
What to Do on Monday
Pick your most expensive AI feature and write down three numbers:
- Model calls per business event.
- Average input tokens per call.
- How many of last month’s calls processed an input identical to one already processed.
Then add an input hash to the view that feeds it.
If that third number is not close to zero, the hash will pay for itself before you touch the model.








































