Closing module 3, we left the whole inventory ready: reproducible code, versioned data, experiments with memory, and an identified champion waiting at models:/churn-cineclick@champion. But a registered model generates no value until someone consumes its predictions. The question that opens this module is not technical but business-driven: who needs the churn predictions, how fresh, and at what volume? The answer to that question determines the deployment pattern — batch, online, or streaming — and getting it wrong is one of the most expensive mistakes in MLOps: teams keeping a 24/7 API alive for predictions that get read once a day, or the reverse, nightly jobs arriving too late for decisions made in seconds. In this lesson you'll learn the three patterns, their costs and benefits, and you'll make the reasoned decision for CineClick alongside Laura.
Contents
- The question that precedes every deployment
- The batch pattern: periodic scoring
- The online pattern: synchronous prediction on demand
- The streaming pattern: prediction over events
- Comparison table and decision criteria
- CineClick's decision, reasoned through
- Hybrid patterns: precompute and serve from a cache
The question that precedes every deployment
Before writing a single line of serving code, you have to answer three questions about the consumer of the predictions:
- Who consumes them? A person (a marketing analyst), another system (the website frontend), a process (an email campaign)?
- How fresh must they be? Can the prediction be hours or days old, or must it reflect what the user did one second ago?
- At what volume and cadence? Millions of rows once a week? Hundreds of requests per second, all day long? Unpredictable bursts?
Notice that none of the three questions mentions the model. CineClick's champion RandomForest is exactly the same artifact in all three patterns; what changes is when predict runs and where the results land. That separation is liberating: the work from modules 2 and 3 (installable package, registry, single-source features) carries over as-is to any pattern.
At CineClick, Laura calls a meeting with two stakeholders and two different answers come out:
- Marketing: "Every Monday we launch the retention campaign. We need the list of customers at high risk of churning, in a table, Monday at 8:00."
- Product: "When a customer clicks 'cancel subscription', we want to decide right then whether to show them a personalized offer. We have about 300 ms of budget before the screen feels slow."
Two consumers, two freshness requirements, two volumes. Hold on to this conflict: we'll resolve it at the end of the lesson.
The batch pattern: periodic scoring
The batch pattern runs the model periodically over a large set of inputs — typically the entire customer base — and writes the results to a store (a database table, a Parquet file, a data warehouse). Consumers don't call the model: they read the results table.
flowchart LR
S[Scheduler<br/>Monday 06:00] --> J[Scoring job]
R[(MLflow registry<br/>churn-cineclick@champion)] --> J
D[(churn_customers<br/>full base)] --> J
J --> T[(churn_predictions<br/>table)]
T --> M[Marketing campaign]
T --> B[Dashboards / BI]A batch job for CineClick looks like this (realistic pseudocode; scheduler-based orchestration comes in lesson 05-03):
# scripts/score_batch.py — weekly scoring job (sketch)
import mlflow
import pandas as pd
from datetime import datetime, timezone
from cineclick_churn.features import build_features # single source (03-03)
MODEL_URI = "models:/churn-cineclick@champion"
def main() -> None:
# 1. Load the champion from the registry: always the version governed by the alias
model = mlflow.sklearn.load_model(MODEL_URI)
# 2. Read the ENTIRE active customer base (here, from the analytical store)
customers = pd.read_parquet("data/active_customers.parquet")
# 3. Build features with THE SAME function training used
X = build_features(customers)
# 4. One single vectorized predict_proba over thousands of rows
probs = model.predict_proba(X)[:, 1] # column 1 = churn probability
# 5. Write results with traceability metadata
result = pd.DataFrame({
"customer_id": customers["customer_id"],
"churn_probability": probs,
"scoring_date": datetime.now(timezone.utc).isoformat(),
"model_uri": MODEL_URI,
})
result.to_parquet("data/churn_predictions.parquet", index=False)
if __name__ == "__main__":
main()Key points about the code:
- Line by line, it's all familiar material: loading from the registry (03-02), features from the single source (03-03), vectorized
predict_proba. Batch requires no new infrastructure — a script and a scheduler are enough. - Step 4 is one single
predict_probaover all the rows: scikit-learn models are optimized for vectorized operation, and scoring 100,000 customers at once takes seconds (we'll come back to this idea with numbers in 04-05). - The metadata (
scoring_date,model_uri) later answers "which model produced this list?" — traceability, the thread running through the whole course.
Advantages: maximum operational simplicity (if the job fails at 6:00, you rerun it and by 7:00 the data is there; nobody was waiting online), minimal cost (the machine only exists while the job runs), enormous throughput. Limitation: freshness. Monday's prediction was computed with Sunday's data; if the customer opens 5 support tickets on Tuesday, the table won't know until the following Monday.
The online pattern: synchronous prediction on demand
The online pattern (also synchronous, or request-response) exposes the model behind an API: a consumer sends the data of ONE customer and receives the prediction in the same call, within tens or hundreds of milliseconds.
sequenceDiagram
participant F as CineClick frontend
participant A as Prediction API
participant M as Model in memory
F->>A: POST /predict {customer data}
A->>M: build_features + predict_proba
M-->>A: probability 0.87
A-->>F: {"churn_probability": 0.87, ...}
Note over F: decides whether to show the offer<br/>in < 300 msWhen do you need online? When the decision is made in the moment and depends on information that may not exist in any previous batch:
- The customer clicks "cancel" now and the offer must be decided now.
- Fraud detection at the instant of payment.
- Recommendations reacting to what the user just did in the session.
The price of that immediacy is operational, and you should be very aware of it before choosing it:
- The service must be alive 24/7: replicas, healthchecks, someone on call if it goes down.
- Latency matters: every request has a budget (product's 300 ms), and you must measure percentiles, not averages (04-05).
- You have to size for peaks: if a premiere night triples cancellations, the API must hold up (04-04).
- Input validation is critical: in batch you control the source data; online, any client can send you anything (we'll solve this with Pydantic in 04-02).
In short: online is not "batch but more modern"; it's a deliberate trade of higher cost and complexity for instant freshness. The next three lessons (FastAPI, Docker, Kubernetes) exist precisely because the online pattern demands that infrastructure.
The streaming pattern: prediction over events
The third pattern, streaming (or event-driven asynchronous), sits between the other two. Predictions are neither requested (online) nor scheduled (batch): they are triggered when an event occurs. Events flow through a queue or message bus (Kafka, RabbitMQ, Pub/Sub, Kinesis) and a consumer holding the model processes them as they arrive, publishing the result to another topic or a table.
flowchart LR
E1[event: ticket opened] --> K[/event queue/]
E2[event: failed payment] --> K
E3[event: 0 hours this week] --> K
K --> C[Consumer with the model]
C --> K2[/topic: risk_updated/]
K2 --> N[Notification system]
K2 --> T[(Risk table)]Its defining traits (we'll stay conceptual — standing up Kafka is out of scope for this course):
- Nobody waits for the response: the event's producer publishes and moves on with its life. Latency is measured in "seconds until the effect happens", not in milliseconds of a blocking call.
- Near-real-time freshness without the coupling of a synchronous API: if the consumer goes down, events pile up in the queue and get processed when it comes back — fault tolerance is built in.
- High operational complexity: you have to operate the message bus, manage offsets, reprocessing, duplicates, event ordering. It's the pattern that demands the most infrastructure.
For churn, a streaming use case would be: "every time a customer opens their third ticket of the month, recompute their risk and, if it exceeds 0.8, notify the retention team within minutes". CineClick has neither that case nor the event infrastructure today; what matters is that you can recognize when a problem is streaming: reacting to individual facts with seconds-level freshness, with nobody waiting online.
Comparison table and decision criteria
| Dimension | Batch | Online (synchronous) | Streaming (events) |
|---|---|---|---|
| Perceived latency | Hours/days (until the next job) | Milliseconds (10–500 ms per request) | Seconds/minutes from the event |
| Data freshness | That of the last job | That of the moment of the request | That of the triggering event |
| Typical volume | Millions of rows per run | 1 row per request, N requests/s | 1 event at a time, continuous flow |
| Infrastructure cost | Low (ephemeral compute, only while running) | Medium/high (24/7 service, replicas, peaks) | High (message bus + 24/7 consumers) |
| Operational complexity | Low (script + scheduler; failure → rerun) | Medium (API, healthchecks, scaling, on-call) | High (Kafka/queues, offsets, reprocessing) |
| Fault tolerance | High (rerun the job) | Low (outage = errors visible to users) | High (the queue retains the events) |
| Input validation | Controlled at the source | Critical (arbitrary external input) | Medium (event contracts) |
| Example uses | Campaigns, portfolio scoring, reports, monthly propensity | Payment fraud, offer at cancellation, dynamic pricing, search | Behavior-driven alerts, near-real-time personalization, IoT |
Decision criteria, in order:
- Start from the freshness the business genuinely needs, not the one that sounds best. "Real time" is a frequent request that, after asking twice, usually means "every morning is fine".
- If nobody is waiting for the response online, don't build an API. This is the number one classic mistake: maintaining a 24/7 service (with its on-call rotation, its scaling, and its bill) for predictions that a 10-minute weekly job would cover. The inverse also exists: trying to serve a 300 ms decision by reading a batch table that's 6 days old.
- Batch is the default pattern. It's the cheapest and simplest to operate; you only abandon it when freshness demands it. Many mature teams serve 80% of their models in batch.
- Streaming only if the event infrastructure already exists (or is justified). Adopting Kafka for a single model almost never pays off.
- The patterns are not mutually exclusive: the same model can be served in batch for one consumer and online for another. Which is, exactly, CineClick's case.
CineClick's decision, reasoned through
Back to Laura's meeting. Let's apply the criteria to each consumer:
Marketing (weekly retention campaign). Freshness needed: weekly. Volume: the whole customer base at once. Nobody waits online: the campaign is prepared Monday morning with the table already written. Clear verdict: batch. A weekly job that loads models:/churn-cineclick@champion, scores the full base, and writes churn_predictions. It's the main business case — the one that justified the project — and building an API for it would be the classic mistake from the table above. This job will get orchestrated with a scheduler, retries, and alerts in lesson 05-03; for now the script sketch you've already seen is enough.
Product (offer at the moment of cancellation). Freshness needed: that very instant — the decision uses the customer's current state and must resolve in under 300 ms while the cancellation screen loads. Could it be served from the batch table? Almost: but the table can be up to 6 days old, and it's precisely the recent behavior (this week's tickets, this week's hours) that is the most valuable signal when someone is about to cancel. Product verified it with data: among customers who cancel, a relevant share shows the deterioration in the last few days. Verdict: online. Marc, who has suffered unnecessary APIs at his previous company, plays devil's advocate and asks whether it's worth it; the answer is yes because there is an in-the-moment decision with a millisecond budget, not because online is more modern.
The team's final decision, documented in the repo:
CineClick serves the
churn-cineclickmodel with two patterns over the same artifact: (1) weekly batch for marketing's retention campaign — the main case, orchestrated in module 5; (2) an online service for the offer in the cancellation flow — implemented with FastAPI in 04-02, packaged with Docker in 04-03, and deployed at scale in 04-04. Both loadmodels:/churn-cineclick@championand importbuild_featuresfrom the package: same model, same features, two cadences.
Notice the architectural consequence of the course's earlier decisions: because the model lives in the registry under an alias and the features have a single source, adding a second consumption pattern duplicates nothing. If module 3 had ended with a loose pickle and copy-pasted features, we would now have two copies of everything to keep in sync.
Hybrid patterns: precompute and serve from a cache
There is a middle ground worth knowing: precompute in batch and serve online from a cache. A periodic job computes the predictions for all customers and loads them into a fast-read store (Redis, an indexed table); the "API" merely does a lookup by customer_id in ~1 ms, without executing the model.
| Pure online | Hybrid (precomputed + cache) | |
|---|---|---|
| Latency | 10–500 ms (features + predict) | ~1–5 ms (lookup) |
| Freshness | The moment of the request | That of the last precompute job |
| New/unseen inputs | Yes (computes over whatever arrives) | No (if the customer isn't in the cache, there's no prediction) |
| Cost per request | Real compute on every call | Nearly zero on reads; the cost is paid in the job |
| When it shines | The input changes between requests | Known, finite universe of inputs; heavy read traffic |
For CineClick the hybrid was ruled out as the main pattern for the "cancellation" case for the freshness reason we've seen (the recent signal is the one that matters), but it doesn't drop off the map: in lesson 04-05 we'll bring it back as an optimization — caching responses with a short TTL to make inference cheaper without giving up freshness entirely. Many real recommendation systems work this way: candidates precomputed in batch, light refinement online.
Common Mistakes and Tips
- Mistake: choosing the pattern by fashion rather than by consumer. "We want real time" without a case that demands it leads to paying for 24/7 infrastructure for data that gets looked at once a day. Always ask: who is waiting for the response, and how long can they wait?
- Mistake: serving millisecond decisions from an aged batch table. The inverse of the previous one. If the fresh signal is what predicts (as in CineClick's cancellation), a 6-day-old prediction can be worse than showing nothing.
- Mistake: treating the patterns as mutually exclusive. The same registry model can feed batch and online at once. What must not be duplicated is the artifact or the feature logic.
- Mistake: forgetting traceability in batch. A prediction table without
scoring_dateandmodel_uriis a time bomb: nobody will know which model generated which list when marketing asks why a customer received the offer. - Tip: write the pattern decision down in the repo (a
docs/decisions/folder or the README), with the consumers, the agreed freshness, and the discarded alternatives. Six months from now someone will propose "moving everything to streaming" and you'll be glad the reasoning is written down. - Tip: size with numbers, not adjectives. "A lot of traffic" is not data; "40 cancellations/hour at peak" is, and you'll probably be surprised at how little that is.
Exercises
Exercise 1
Classify each case as batch, online, or streaming, and justify it with the three questions (who consumes, freshness, volume):
a) A bank decides at the moment of a card payment whether the transaction is fraudulent. b) An insurance company computes every night the renewal propensity of its entire portfolio for the sales team's next day. c) A logistics app recomputes a shipment's delay risk every time a scan event arrives from a distribution center, to update the customer notification within minutes.
Exercise 2
CineClick's BI team asks for "an API endpoint to query any customer's churn probability from their dashboards, which refresh every morning". Which pattern do you recommend and why? What question would you ask before deciding?
Exercise 3
Modify the score_batch.py pseudocode so that, in addition to writing the full Parquet, it produces a second file high_risk_customers.parquet containing only the customers whose probability exceeds 0.7, sorted from highest to lowest risk — the format marketing wants for the campaign.
Solutions
Solution 1
a) Online. The consumer is another system (the payment processor) that blocks the transaction waiting for the response; freshness is the current instant (the transaction data was just born); volume is N requests/s with a millisecond budget. It's the canonical online example. b) Batch. The consumer is a human team the next day; daily freshness is enough; volume is the whole portfolio at once. Nobody waits online: a nightly job into a table. c) Streaming. The trigger is an individual event (the scan), nobody waits blocked for the response, and the required freshness is minutes, not milliseconds or days. Events flow through a queue and a consumer holding the model processes them on arrival.
Solution 2
Batch, served from the churn_predictions table that will already exist for marketing — the dashboards refresh every morning, so weekly freshness (or daily, if the job's cadence is increased) is enough and nobody is waiting online. Building an online endpoint for this would be the classic mistake: BI would fire hundreds of queries against a 24/7 API to get data that a JOIN with the table resolves for free. The prior question: how fresh do you actually need the data to be? If the answer were "we want to see the effect of a retention action the same afternoon", you'd raise the batch cadence — and only if they asked for the exact instant would reusing the online service from 04-02 be justified.
Solution 3
# ... after building `result` ...
high_risk = (
result[result["churn_probability"] > 0.7]
.sort_values("churn_probability", ascending=False)
)
high_risk.to_parquet("data/high_risk_customers.parquet", index=False)Two details worth noting: the 0.7 threshold is a business decision (how many customers the campaign can absorb), not a model parameter — it deserves to live in configuration, not hardcoded; and the file inherits the traceability columns (scoring_date, model_uri), so the list marketing receives remains auditable.
Conclusion
You now have the map: batch for periodic, massive predictions with nobody waiting (cheap, simple, the default pattern), online for in-the-moment decisions with a millisecond budget (expensive and demanding, but irreplaceable when freshness is the signal), and streaming for reacting to individual events within seconds (powerful, but only with the infrastructure to sustain it). And you know the choice is dictated by the consumer — who, how fresh, at what volume — not by fashion. CineClick has it decided and documented: weekly batch for the marketing campaign (to be orchestrated in module 5) and an online service for the offer in the cancellation flow, both drinking from the same models:/churn-cineclick@champion and the same build_features. The next lesson builds that online service for real: a FastAPI application that validates inputs with Pydantic, loads the champion at startup, imports the features from the single source, and responds in milliseconds with the churn probability. It's time for Laura's model to take its first request.
MLOps Course
Module 1: MLOps Fundamentals
- What MLOps is and why models die in the notebook
- The lifecycle of an ML model in production
- MLOps maturity levels and team roles
- The course project: from notebook to production
Module 2: From Notebook to Reproducible Code
- Structuring an ML project: from notebook to package
- Reproducible environments and dependency management
- Data versioning with DVC
- Reproducible training pipelines
Module 3: Experiments and Model Registry
- Experiment tracking with MLflow
- Model registry: versioning and promoting models
- Feature stores: when and what for
Module 4: Serving Models in Production
- Deployment patterns: batch, online and streaming
- A prediction service with FastAPI
- Packaging with Docker
- Scaling and deployment: Kubernetes and serverless
- Inference optimization: latency and cost
Module 5: Automation: CI/CD and Orchestration
- CI for ML: testing code, data and models
- CD: automating model deployment
- Orchestrating ML pipelines
- Release strategies: shadow, canary and A/B
