TAI BUI
Back to curriculum
BuildPython

Build a Complete Vision Pipeline — Capstone

A production vision system is a chain of models and rules held together by data contracts, not one clever model doing everything. Every piece already exists earlier in this phase. This capstone's only job is wiring them into a service that runs end to end.

TIME: ~120 minutesPREREQUISITES: Phase 4 Lessons 01-15

Learning Objectives

  • Design a production vision pipeline that detects objects, classifies them, and emits structured JSON — with every failure path handled
  • Plug a detector (Mask R-CNN or YOLO), a classifier (ConvNeXt-Tiny), and a data contract (Pydantic) into one service
  • Benchmark the end-to-end pipeline and identify the first bottleneck (usually preprocessing, then the detector)
  • Ship a minimal FastAPI service that accepts an image upload, runs the pipeline, and returns detections with classifications

The Problem

Individual vision models are useful. Vision products are chains of them. A retail shelf audit is a detector plus a product classifier plus a price-OCR pipeline. Autonomous driving chains a 2D detector, a 3D detector, a segmenter, a tracker, and a planner. A medical pre-screen chains a segmenter, a region classifier, and a clinician UI.

Wiring those chains is what separates an ML prototype from a product. Every interface between models is a new place for bugs: every coordinate transform, every normalisation, every mask resize is a silent-failure candidate. A pipeline is only as strong as its weakest interface.

This capstone builds the minimum viable pipeline: detection, classification, structured output, and a serving layer. Everything else in Phase 4 slots into this skeleton. Swap Mask R-CNN for YOLOv8, add an OCR head, add a segmentation branch, add a tracker: the architecture stays stable, only the pieces change.

Warm-up0/3
1. You already have a trained object detector and a trained image classifier. To build a system that finds objects in a photo and then says what each one is, what is the most direct way to combine them without retraining either?Warm-up
A photo box with arrows down to two separate boxes, trained detector (finds where objects are) and trained classifier (says what each one is), both pointing into a small dashed question-mark box, captioned asking how they combine without retraining either one.
2. When you optimise a system made of several models run one after another, why should you time each stage separately before changing anything?Warm-up
Four generic chained boxes, Stage A through Stage D, each with an unresolved question-mark milliseconds tag underneath, and a clock icon beside a line saying there is budget to optimise exactly one of the four.
3. A web API receives an upload that is not a valid image file. Which HTTP status family tells the client that its own request was at fault?Warm-up
A client box uploading a file named broken.xyz, an arrow to a server box, an arrow to a response box labelled question-mark-x-x, with four plain tags below listing the status families 2xx, 3xx, 4xx and 5xx and no meaning given for any of them.
Check: The Problem0/3
1. In the capstone's skeleton, what has to change when you swap Mask R-CNN for YOLOv8 as the detector?Explain
A preprocess box, a detector box marked Mask R-CNN swapped to YOLOv8, a classify box, and a small dashed question mark at the end, with a line asking what happens to the contracts, FastAPI and the rest of the skeleton.
2. Why does the lesson call every interface between models a new place for bugs?Explain
A stage A output card listing box coordinates, a mask and pixel values, an arrow carrying a dashed question mark, and a stage B input card listing a crop region, a resized mask and normalised pixels, under a line saying every item on the left has to be converted into the item on the right.
3. A retail shelf audit needs price OCR added after detection and classification. Which design keeps the pipeline reliable?Design
A detect box and a classify box, then an arrow into a dashed question-mark box, with a note that price OCR needs to be added at that point and a question asking what design fits.

The Concept

The pipeline

A vision pipeline is an ordered chain of preprocessing, inference, and postprocessing stages, each one connected to the next by a typed interface.

Mermaid diagram

Seven stages in total. The two model stages are the expensive ones; the other five are where the bugs live.

Data contracts with Pydantic

Every model boundary becomes a typed object, so silent failures turn into loud ones.

Detection(
    box: tuple[float, float, float, float],   # (x1, y1, x2, y2), absolute pixels
    score: float,                              # [0, 1]
    class_id: int,                             # from detector's label map
    mask: Optional[list[list[int]]],           # RLE-encoded if present
)

PipelineResult(
    image_id: str,
    detections: list[Detection],
    classifications: list[Classification],
    inference_ms: float,
)

When a detector returns boxes in (cx, cy, w, h) instead of (x1, y1, x2, y2), Pydantic's validation fails at the boundary and you find out immediately instead of debugging a downstream crop that silently returns empty regions.

Where latency goes

Three truths hold in nearly every vision pipeline:

  1. Preprocessing is often the biggest single block. Decoding JPEGs, converting colour spaces, and resizing are all CPU-bound work, and it's easy to forget they're even part of the budget.
  2. The detector dominates GPU time. 70-90% of GPU time sits in the detection forward pass.
  3. Postprocessing is cheap on GPU, expensive on CPU. NMS and RLE encode/decode fall in this bucket. Always profile on the actual target.

Knowing that distribution up front is what turns optimisation into a prioritised list instead of a guessing game.

Failure modes

  • Empty detections: return an empty list, don't crash. Log it.
  • Out-of-bounds boxes: clamp to image size before cropping.
  • Tiny crops: skip classification for boxes smaller than the classifier's minimum input.
  • Corrupt upload: return a 400 with a specific error code, not a 500.
  • Model load failure: fail at service startup, not on the first request.

A production pipeline handles each of these without a generic try/except that swallows the failure. Every failure gets a named code and a real response.

Batching

A production service serves multiple clients at once. Batching detections and classifications across requests multiplies throughput; the trade-off is extra latency while a batch fills. A typical setup collects requests for up to 20ms, batches them, processes the batch, and distributes the responses back out. torchserve and triton do this natively. Smaller services with predictable load roll their own micro-batcher.

Check: The Concept0/19
1. What is a vision pipeline?Define
An image upload box reading dog.jpg, image/jpeg, an arrow into a dashed box labelled vision pipeline with a question mark inside it, then an arrow to a response box reading application/json, with a line saying the inside of the middle box is not drawn.
2. What is a data contract in a vision pipeline?Define
A detect box and a crop box with a dashed box labelled data contract and a question mark sitting between them, above one example value that crosses the boundary, box 60 40 240 240 with score 0.92 and class id 1.
The example value is computed from the stub detector in the lesson's main.py on a 400 by 600 image.
3. What does preprocessing cover in a vision pipeline?Define
An upload box reading dog.jpg bytes, an arrow into a dashed box labelled Preprocess with a question mark inside, then an arrow into a detector box, with a line asking what work happens inside that one stage.
4. What does postprocessing cover in a vision pipeline?Define
A detector box labelled raw output tensors, an arrow into a dashed box labelled Postprocess with a question mark inside, then an arrow into a final detections box, with a line asking what work happens inside that one stage.
5. What is a microbatcher?Define
Three request boxes on the left, req 1, req 2 and req 3, all feeding a dashed box labelled microbatcher with a question mark inside, which feeds one model on one GPU, under a line asking what the middle component does with the three requests.
6. What is a failure code in a pipeline service?Define
Three generic boxes, failure type 1, 2 and 3, all pointing into one service box, which points down into a dashed question-mark box, under a line asking what goes back to the client for each of the three.
7. What share of GPU time does the lesson say the detector forward pass takes?Define
A GPU panel containing three equally drawn boxes, detector, classifier and pre slash post, each with an unresolved question-mark-percent tag underneath, asking how GPU time is split across the three.
8. Why do production vision pipelines use Pydantic models (or equivalent) at every stage boundary?Explain
Two copies of the same two-stage boundary, one plain and one with a small schema-check box sitting on the arrow, each followed by its own dashed question-mark box, with no outcome resolved for either.
9. Why does the lesson reject a generic try/except around the pipeline in favour of named failure handling?Explain
A failure box with two arrows down, one to a box labelled try except catch all and one to a box labelled a named code per case, each followed by its own small dashed question-mark box.
10. A detector returns a box that extends past the image's right edge. What does the pipeline do, and why?Explain
An image frame with a dashed detected-box outline whose right portion sits past the frame's right edge, and a separate dashed question-mark box beside it, asking what happens before this box is cropped.
11. Which stage does the lesson say is often the biggest single block of latency in a vision pipeline?Explain
Five equally drawn boxes, read request, preprocessing, detect, postprocess and serialise JSON, with a dashed question-mark box beneath them asking which group is the single biggest block.
12. Why does the lesson say to always profile postprocessing on the actual target hardware?Explain
Two identically drawn boxes, CPU and GPU, both listing NMS and RLE encode, each with an unresolved cost question mark underneath, noting the same operations profiled on two different targets.
13. You add batching in front of the classifier: wait up to 10 ms, batch all pending crops, run one GPU forward pass. What is the expected effect?Apply
Three incoming requests feeding a 10 millisecond wait window, then one GPU forward pass box, with two unresolved question marks reading throughput and latency per request.
14. The pipeline's classifier crashes whenever a detection has a box of 3 x 4 pixels. What is the right fix?Analyze
A detected box of 3 by 4 pixels, an arrow into a crash mark, then into the classifier box, with the fix left as a question mark.
15. The service returns a 500 whenever a user uploads a corrupted JPEG. What is the right fix?Analyze
A corrupted-upload box with an arrow into a 500 tag, the current behaviour, with the fix left as a question mark.
16. A deployment's checkpoint file is corrupted and the model cannot load. When should the failure surface?Analyze
Three timeline points, startup, first request and health check, each marked with its own unresolved question mark, asking which is when a broken checkpoint should surface.
17. A pipeline returns confident-looking results for a week, but every crop is empty and there are no errors in the logs. The detector emits (cx, cy, w, h) while the cropping code expects (x1, y1, x2, y2). Which ingredient would have caught this first?Analyze
A detector-emits box reading cx, cy, w, h next to a crop-expects box reading x1, y1, x2, y2, with notes that every crop comes out empty with a still-confident score, and that nothing was logged for a full week, above a dashed question mark asking which ingredient would have caught this first.
18. On GPU, a team swaps the classifier for a model that is twice as fast, but total latency barely moves. What is the most likely explanation?Analyze
Two unlabelled bars of the same height on one axis, one marked before and one marked after classifier 2x faster, with a note that the question gives no numbers and the equal heights say only that the total did not move.
19. Your pipeline runs on GPU and must handle many concurrent requests. Each user can tolerate a few extra milliseconds. Which design change fits?Design
Three client boxes stacked on the left, all with arrows reaching one GPU pipeline box noted as each user tolerating a few extra milliseconds, above a dashed question-mark box and a line reading design equals question mark.

Build It

Step 1: Data contracts

python
from pydantic import BaseModel, Field
from typing import List, Optional, Tuple
 
class Detection(BaseModel):
    box: Tuple[float, float, float, float]
    score: float = Field(ge=0, le=1)
    class_id: int = Field(ge=0)
    mask_rle: Optional[str] = None
 
 
class Classification(BaseModel):
    detection_index: int
    class_id: int
    class_name: str
    score: float = Field(ge=0, le=1)
 
 
class PipelineResult(BaseModel):
    image_id: str
    detections: List[Detection]
    classifications: List[Classification]
    inference_ms: float

This costs five seconds to type and saves an hour of debugging on any pipeline that sees real traffic.

Step 2: A minimal Pipeline class

python
import time
import numpy as np
import torch
from PIL import Image
 
class VisionPipeline:
    def __init__(self, detector, classifier, class_names,
                 device="cpu", min_crop=32):
        self.detector = detector.to(device).eval()
        self.classifier = classifier.to(device).eval()
        self.class_names = class_names
        self.device = device
        self.min_crop = min_crop
 
    def preprocess(self, image):
        """
        image: PIL.Image or np.ndarray (H, W, 3) uint8
        returns: CHW float tensor on device
        """
        if isinstance(image, Image.Image):
            image = np.asarray(image.convert("RGB"))
        tensor = torch.from_numpy(image).permute(2, 0, 1).float() / 255.0
        return tensor.to(self.device)
 
    @torch.no_grad()
    def detect(self, image_tensor):
        return self.detector([image_tensor])[0]
 
    @torch.no_grad()
    def classify(self, crops):
        if len(crops) == 0:
            return []
        batch = torch.stack(crops).to(self.device)
        logits = self.classifier(batch)
        probs = logits.softmax(-1)
        scores, cls = probs.max(-1)
        return list(zip(cls.tolist(), scores.tolist()))
 
    def run(self, image, image_id="anonymous"):
        t0 = time.perf_counter()
        tensor = self.preprocess(image)
        det = self.detect(tensor)
 
        crops = []
        detections = []
        valid_indices = []
        for i, (box, score, cls) in enumerate(zip(det["boxes"], det["scores"], det["labels"])):
            x1, y1, x2, y2 = [max(0, int(b)) for b in box.tolist()]
            x2 = min(x2, tensor.shape[-1])
            y2 = min(y2, tensor.shape[-2])
            detections.append(Detection(
                box=(x1, y1, x2, y2),
                score=float(score),
                class_id=int(cls),
            ))
            if (x2 - x1) < self.min_crop or (y2 - y1) < self.min_crop:
                continue
            crop = tensor[:, y1:y2, x1:x2]
            crop = torch.nn.functional.interpolate(
                crop.unsqueeze(0),
                size=(224, 224),
                mode="bilinear",
                align_corners=False,
            )[0]
            crops.append(crop)
            valid_indices.append(i)
 
        class_preds = self.classify(crops)
 
        classifications = []
        for valid_idx, (cls_id, cls_score) in zip(valid_indices, class_preds):
            classifications.append(Classification(
                detection_index=valid_idx,
                class_id=int(cls_id),
                class_name=self.class_names[cls_id],
                score=float(cls_score),
            ))
 
        return PipelineResult(
            image_id=image_id,
            detections=detections,
            classifications=classifications,
            inference_ms=(time.perf_counter() - t0) * 1000,
        )

Every interface here is typed, and every failure path has a specific handling decision instead of a shrug.

Step 3: Wire a detector and a classifier

python
from torchvision.models.detection import maskrcnn_resnet50_fpn_v2
from torchvision.models import convnext_tiny
 
# Use ImageNet-pretrained weights for a realistic pipeline without training
detector = maskrcnn_resnet50_fpn_v2(weights="DEFAULT")
classifier = convnext_tiny(weights="DEFAULT")
class_names = [f"imagenet_class_{i}" for i in range(1000)]
 
pipe = VisionPipeline(detector, classifier, class_names)
 
# Smoke test with a synthetic image
test_image = (np.random.rand(400, 600, 3) * 255).astype(np.uint8)
result = pipe.run(test_image, image_id="demo")
print(result.model_dump_json(indent=2)[:500])

Step 4: FastAPI service

python
from fastapi import FastAPI, UploadFile, HTTPException
from io import BytesIO
 
app = FastAPI()
pipe = None  # initialised on startup
 
@app.on_event("startup")
def load():
    global pipe
    detector = maskrcnn_resnet50_fpn_v2(weights="DEFAULT").eval()
    classifier = convnext_tiny(weights="DEFAULT").eval()
    pipe = VisionPipeline(detector, classifier, class_names=[f"c{i}" for i in range(1000)])
 
@app.post("/detect")
async def detect_endpoint(file: UploadFile):
    if file.content_type not in {"image/jpeg", "image/png", "image/webp"}:
        raise HTTPException(status_code=400, detail="unsupported image type")
    data = await file.read()
    try:
        img = Image.open(BytesIO(data)).convert("RGB")
    except Exception:
        raise HTTPException(status_code=400, detail="cannot decode image")
    result = pipe.run(img, image_id=file.filename or "upload")
    return result.model_dump()

Run with uvicorn main:app --host 0.0.0.0 --port 8000. Test with curl -F '[email protected]' http://localhost:8000/detect.

Step 5: Benchmark the pipeline

python
import time
 
def benchmark(pipe, num_runs=20, image_size=(400, 600)):
    img = (np.random.rand(*image_size, 3) * 255).astype(np.uint8)
    pipe.run(img)  # warm up
 
    stages = {"preprocess": [], "detect": [], "classify": [], "total": []}
    for _ in range(num_runs):
        t0 = time.perf_counter()
        tensor = pipe.preprocess(img)
        t1 = time.perf_counter()
        det = pipe.detect(tensor)
        t2 = time.perf_counter()
        crops = []
        for box in det["boxes"]:
            x1, y1, x2, y2 = [max(0, int(b)) for b in box.tolist()]
            x2 = min(x2, tensor.shape[-1])
            y2 = min(y2, tensor.shape[-2])
            if (x2 - x1) >= pipe.min_crop and (y2 - y1) >= pipe.min_crop:
                crop = tensor[:, y1:y2, x1:x2]
                crop = torch.nn.functional.interpolate(
                    crop.unsqueeze(0), size=(224, 224), mode="bilinear", align_corners=False
                )[0]
                crops.append(crop)
        pipe.classify(crops)
        t3 = time.perf_counter()
        stages["preprocess"].append((t1 - t0) * 1000)
        stages["detect"].append((t2 - t1) * 1000)
        stages["classify"].append((t3 - t2) * 1000)
        stages["total"].append((t3 - t0) * 1000)
 
    for stage, times in stages.items():
        times.sort()
        print(f"{stage:12s}  p50={times[len(times)//2]:7.1f} ms  p95={times[int(len(times)*0.95)]:7.1f} ms")

Typical numbers on CPU: preprocess ~3 ms, detect 300-500 ms, classify 20-40 ms, total 350-550 ms. Move to GPU and detect drops to 20-40 ms, at which point preprocessing and classification start eating a much bigger share of the total.

Check: Build It0/10
1. On CPU the detector takes 300-500 ms. After moving to GPU it takes 20-40 ms. Why do preprocessing and classification then matter more?Explain
A millisecond axis from 0 to 500 with two bands drawn on it for the detector alone: a wide CPU band from 300 to 500 ms and a short GPU band from 20 to 40 ms, both ranges taken straight from the question.
2. In the FastAPI service, why does the endpoint check the content type and wrap the decode in a try/except that raises a 400?Explain
A client-upload box, an arrow into a content-type-check box, an arrow down into a decode try except box, then an arrow into a small dashed question-mark box.
3. The image tensor is 3 x 400 x 600 (channels, height, width). The detector returns the box (550.7, 100.2, 640.9, 200.5). After the clamping code in run, which box is stored in the Detection?Apply
The three clamping lines from run() with no comments added, above the question's own tensor shape 3 by 400 by 600 as channels, height and width, its box 550.7, 100.2, 640.9, 200.5, and the stored box left as a question mark.
4. With the default min_crop of 32, the detector returns one box (10, 10, 50, 40). What does run do with it?Apply
Four lines from run() with no comments added: the detections append line, the min_crop size check, its continue, and the crop line that follows, above the question's own min_crop of 32 and box 10, 10, 50, 40, with the outcome left as a question mark.
5. The detector returns three boxes. Only the middle one is smaller than min_crop. Which detection_index values appear in the classifications?Apply
Three boxes, box 0 and box 2 marked as meeting min_crop and box 1 marked as under min_crop, above five unannotated lines from run() covering the continue, the crops and valid_indices appends, and the zip that builds each Classification, with the resulting detection_index values left as a question mark.
6. The detector finds nothing in an image. What does pipe.run return?Apply
Five unannotated lines from run(): the detect call, the three empty lists, the loop header over det boxes, and the classify call, with a note that this image's det boxes has length 0 and the return value left as a question mark.
7. What happens when the code builds Detection(box=(0, 0, 10, 10), score=1.4, class_id=3)?Apply
A code line declaring Detection's score field with ge 0 and le 1, followed by the exact construction call Detection with box 0,0,10,10, score 1.4, class_id 3, and the result left as a question mark.
8. preprocess receives a NumPy array of shape (400, 600, 3) with dtype uint8. What does it return?Apply
The preprocess permute-and-divide line, with a note giving the input as a numpy array of shape 400 by 600 by 3 and dtype uint8, and the output shape and value range left as a question mark.
9. A client posts a GIF to /detect with content type image/gif. What does the service respond?Apply
Both 400 guards in the endpoint, the content-type check and the decode try except, with each raise's detail string replaced by question marks, above a note that this upload has content type image slash gif with intact bytes and the response left as a question mark.
10. benchmark runs 20 timed runs per stage, sorts the times, and reads times[int(len(times) * 0.95)]. Which run does the p95 value come from?Apply
The sort line and the p50 and p95 index lines from benchmark(), with a note that num_runs is 20, and the question of which sorted run p95 reads left as a question mark.

Use It

Production templates converge on the same structure, plus a handful of additions:

  • Model versioning: always log the model name and weights hash in the response.
  • Per-request trace IDs: log every stage's timing for every request so a slow response can be traced back to the stage that caused it.
  • Fallback path: if the classifier times out, return the detections without classifications instead of failing the whole request.
  • Safety filters: NSFW and PII filters run after classification, before the response leaves the service.
  • Batch endpoint: a /detect_batch route that accepts a list of image URLs for bulk processing.

For production serving, torchserve, Triton Inference Server, and BentoML handle batching, versioning, metrics, and health checks out of the box. Running FastAPI directly is fine for a prototype or a small-scale product; it just won't grow those features on its own.

Check: Use It0/7
1. What is a trace ID?Define
Two request boxes, A and B, both feeding one service log panel whose six lines name only a stage and a timing, under a line asking which line belongs to which request.
Timings typed to the per-stage ranges the lesson states, not captured from a run.
2. What is a health check endpoint?Define
A two-row route table for the capstone service: POST slash detect runs the pipeline and returns JSON, and GET slash health with its description left as a question mark, under lines saying the second route does no inference and asking what it reports and what calls it.
3. Why log the model name and weights hash with every response, keep per-request trace IDs, and run safety filters after classification but before the response leaves?Explain
A short pipeline tail ending at Respond, with three tags pointing at it: version tag, trace id and safety filter, under a line asking why each of the three is there.
4. A classifier call times out. Why does the lesson's fallback path return the detections without classifications?Explain
A detect box, then a highlighted classify box marked timeout, then a respond box, above a dashed question-mark box, under a line asking what the respond stage should send back.
5. What does running FastAPI directly give up, compared with torchserve, Triton or BentoML?Explain
The lesson's Step 4 FastAPI service shown as five code lines, and below it a dashed box naming torchserve, Triton and BentoML with a question mark inside, under a line asking what the second row comes with that the first one does not.
6. Average stage timings look healthy, yet some users report very slow responses. What is the lesson's tool for finding where those slow requests spend their time?Analyze
A latency-per-request line that stays low across most requests with one tall spike, above a note that the average across all requests still looks fine and a dashed question-mark box.
The curve is drawn to match the question's wording, not captured from a run.
7. You must run a public vision service that needs batching, versioning, metrics and health checks, and you do not want to build them yourself. Which approach fits?Design
Four feature tags, batching, versioning, metrics and health checks, above a dashed question mark box, with a line asking what approach fits.

Ship It

This lesson produces:

  • outputs/prompt-vision-service-shape-reviewer.md: a prompt that reviews a vision service's code for contract and response-shape violations and names the first breaking bug.
  • outputs/skill-pipeline-budget-planner.md: a skill that takes a target latency and throughput and assigns a time budget to every pipeline stage, then flags which stage will miss its budget first.
Author's Note

The part of this lesson that matters most is the one nobody wants to spend time on: the data contract. I've watched a detector ship (cx, cy, w, h) into code that expected (x1, y1, x2, y2), and the bug didn't crash anything — it just quietly cropped every image down to an empty rectangle and returned confident-looking garbage for a week before anyone noticed. There was no stack trace pointing at the real problem, because there was no stack trace at all. That's the specific danger of a vision pipeline: it fails by being wrong, not by breaking.

If you only take one habit from this lesson, take this one. Type every boundary. It costs a few minutes per interface, and it turns next month's silent regression into today's loud, obvious error.

Exercises

  1. (Easy) Run the pipeline on 10 images from any open dataset. Report the average time per stage and the distribution of detection counts per image.
  2. (Medium) Add a mask output field to Detection and encode it as RLE. Verify the JSON stays under 1MB even for a 10-object image.
  3. (Hard) Add a micro-batcher in front of the classifier: collect crops for up to 10 ms, classify them all in one GPU call, return results per request. Measure the throughput gain at 5 concurrent requests per second and the latency added.

Key Terms

TermWhat people sayWhat it actually means
Pipeline"The system"An ordered chain of preprocessing, inference, and postprocessing steps with a typed interface between each pair
Data contract"The schema"Pydantic / dataclass definitions that every stage input and output conforms to; catches integration bugs at the boundary
Preprocessing"Before the model"Decoding, colour conversion, resizing, normalising; usually the biggest CPU time sink
Postprocessing"After the model"NMS, mask resize, threshold, RLE encode; cheap on GPU, expensive on CPU
Microbatcher"Collect then forward"Aggregator that waits a fixed window for multiple requests, runs a single batched forward pass
Trace ID"Request id"Per-request identifier logged at every stage so slow requests can be traced end-to-end
Failure code"Named error"Specific error code per failure class instead of generic 500; enables client retry logic
Health check"Readiness probe"Cheap endpoint that reports whether the service can answer; loadbalancers rely on this

Further Reading