DEV Community

Cover image for Building Real Time AI Systems: What Changes When Computer Vision Meets Production Software
Zainab saif
Zainab saif

Posted on

Building Real Time AI Systems: What Changes When Computer Vision Meets Production Software

A computer vision prototype is straightforward to build: point a model at a video feed, run inference on a frame, return a set of detections. It demos well. Running the same system continuously, at scale, with real users depending on its output, surfaces a different category of problem — one that has comparatively little to do with the model itself.

This article examines that second phase: what changes, architecturally and operationally, when a computer vision model moves from a research artifact to one component inside a production system. It draws on the operational realities of running real-time inference pipelines, the parts of the work that rarely make it into a model card or a conference talk: backpressure, partial failure handling, output-quality drift, model rollout strategy, and the persistent gap between "the model works" and "the system is production-ready.

Where the architecture actually changes

A prototype usually looks like this:

camera / video file → model → result

That's fine for a notebook. It falls apart the moment the input is continuous and the output needs to go somewhere useful. A production pipeline looks closer to what's shown below: video enters through an ingestion layer, moves through preprocessing and inference, gets validated before it reaches application logic, and every stage feeds telemetry back into monitoring.

The important shift is conceptual: the model becomes a service inside a larger system, not the system itself. Once you accept that framing, a lot of the "AI engineering" problem turns into fairly conventional distributed systems work — queues, retries, backpressure, observability , applied to a workload that happens to include a neural network.

Designing the real time pipeline

Ingestion is usually the first place teams underestimate the work. Video sources are messy: variable frame rates, dropped connections, inconsistent resolutions, occasional corrupt frames. Before any frame reaches the model, the pipeline needs to normalize all of that — decode reliably, handle reconnections, and discard frames that fail basic sanity checks (wrong dimensions, all-black frames, decode errors).

Not every frame needs to reach the model. For many use cases — occupancy counting, motion-triggered detection, or analytics on digital signage and out-of-home displays — sampling at a lower rate than the source video, or skipping frames when the scene hasn't meaningfully changed, reduces compute cost without a meaningful loss in accuracy. This decision is worth making deliberately, since it affects nearly everything downstream: queue sizing, GPU provisioning, and per-stream cost. It's a pattern that shows up consistently in production computer-vision analytics work, including systems built by teams such as Macromodule's AI/ML engineering group, where the ingestion layer is treated as a design decision in its own right rather than a default.

A complete, runnable frame-sampling gate:

**

**import time
from dataclasses import dataclass

CHANGE_THRESHOLD = 5.0  # tune per use case; depends on frame representation

@dataclass
class LastFrame:
    timestamp: float  # milliseconds, from time.monotonic() * 1000
    data: list        # simplified representation; use a real diff metric in practice

def frame_difference(current, previous) -> float:
    """Mean absolute difference between two same-length frame summaries.
    In practice, replace this with a proper metric: pixel-level diff,
    histogram distance, or a lightweight motion-detection heuristic —
    not the full model.
    """
    return sum(abs(a - b) for a, b in zip(current, previous)) / len(current)

def should_process_frame(frame, last_processed_frame: LastFrame, min_interval_ms=100) -> bool:
    now = time.monotonic() * 1000
    if now - last_processed_frame.timestamp < min_interval_ms:
        return False
    if frame_difference(frame, last_processed_frame.data) < CHANGE_THRESHOLD:
        return False
    return True
Enter fullscreen mode Exit fullscreen mode

The logic itself is simple; the decision of what threshold and what interval to use belongs to product requirements, not to the model, and is worth pinning down explicitly rather than left as a framework default. Note that frame_difference here is a placeholder — a real implementation should use a proper motion or histogram-based metric rather than comparing raw pixel arrays, which is both slow and noisy.

Latency is more than inference time

Teams often benchmark a model in isolation — "inference takes 15ms" — and treat that as the system's latency budget. It isn't. End-to-end latency is the sum of several stages, most of which have nothing to do with the model:

A model that runs in 15ms can easily sit inside a pipeline that takes 400ms end to end, because preprocessing is unbatched, the database write is synchronous, or the result has to round-trip through an API gateway. This is the single most common gap between "the model is fast" and "the product feels slow," and it's worth instrumenting each stage separately — with timestamps recorded at each boundary — rather than reporting one aggregate number that hides where the time actually goes.

Throughput, concurrency, and backpressure

A single camera stream is a manageable engineering problem. Ten concurrent streams, or a hundred, introduce a different class of problem: what happens when frames arrive faster than the pipeline can process them?

Left unhandled, this shows up as an ever-growing queue, increasing memory use, and eventually stale results — detections computed on frames that are seconds old and no longer represent the current scene. For most real-time use cases, an old detection is often worse than no detection.

The standard approaches apply here, and they're worth taking seriously rather than treating as an afterthought:

  • Bounded queues with drop policies. Cap queue depth and drop the oldest frames when full, rather than letting memory grow unbounded.
  • Worker pools sized to actual throughput, not to peak concurrency — oversized pools just contend for the same GPU.
  • Backpressure signals fed back to the ingestion layer, so upstream producers slow down instead of silently overwhelming the pipeline.
import asyncio

class BoundedFrameQueue:
    """A queue that drops the oldest frame instead of blocking or growing
    unbounded when the pipeline falls behind."""

    def __init__(self, max_size: int = 50):
        self.queue: asyncio.Queue = asyncio.Queue(maxsize=max_size)

    async def put(self, frame) -> None:
        if self.queue.full():
            # Drop the oldest frame rather than blocking ingestion.
            # Safe here because there's no await between the check
            # and the drop, so no other task can interleave.
            _ = self.queue.get_nowait()
        await self.queue.put(frame)

    async def get(self):
        return await self.queue.get()
Enter fullscreen mode Exit fullscreen mode

It's a small pattern, but the alternative — an unbounded queue — is one of the more common root causes of a real-time system degrading gradually into an outage rather than failing loudly and immediately.

What happens when AI or dependent services fail

A model can return an empty result, a malformed response, or simply time out. An external API it depends on can go down. None of this is exotic; it's the normal operating condition of any networked service, and computer vision pipelines are no exception.

Worth handling explicitly:

  • inference timeouts
  • malformed or unexpected model output
  • downstream API failures
  • transient network errors
  • degraded input (corrupted frame, unsupported format)
import asyncio

class InferenceError(Exception):
    """Raised by the model client on a known, non-retryable failure."""

async def run_inference_with_retry(
    model,
    frame,
    max_retries: int = 2,
    timeout_s: float = 1.0,
) -> dict:
    """Runs inference with a timeout and bounded exponential backoff.
    Returns a degraded/error result instead of raising, so callers
    always get a well-formed response to work with.
    """
    for attempt in range(max_retries + 1):
        try:
            return await asyncio.wait_for(model.infer(frame), timeout=timeout_s)
        except asyncio.TimeoutError:
            if attempt == max_retries:
                return {"status": "degraded", "detections": []}
            await asyncio.sleep(0.1 * (2 ** attempt))
        except InferenceError:
            return {"status": "error", "detections": []}
Enter fullscreen mode Exit fullscreen mode

The specific numbers matter less than the principle: a real-time system should have an explicit, tested answer for "the model didn't respond in time," rather than letting that case propagate as an unhandled exception three layers up the stack. This is also where a circuit breaker earns its keep — if a downstream service is consistently timing out, retrying every request just adds load to an already struggling dependency. Failing fast for a cool-down period, then probing occasionally to see if it's recovered, is usually the better trade-off.

Validating output before it reaches the application

It's easy to treat model output as trustworthy simply because it came from the model. In production, output needs the same skepticism as any other untrusted input:

  • confidence thresholds appropriate to the use case, not a default value copied from a tutorial
  • schema validation on the response shape
  • sanity checks on bounding boxes (in-frame, non-degenerate dimensions)
  • deduplication of overlapping detections
  • checks for classes the application doesn't expect
  • temporal consistency checks — a detection that flickers in and out frame to frame is often noise, not a real event
def validate_detection(det: dict, frame_width: int, frame_height: int, min_confidence: float = 0.5) -> bool:
    """Returns True only if the detection passes basic sanity checks.
    Expects det = {"confidence": float, "bbox": [x1, y1, x2, y2]}.
    """
    if det["confidence"] < min_confidence:
        return False

    x1, y1, x2, y2 = det["bbox"]
    if x2 <= x1 or y2 <= y1:
        return False
    if x1 < 0 or y1 < 0 or x2 > frame_width or y2 > frame_height:
        return False

    return True
Enter fullscreen mode Exit fullscreen mode

This step is easy to omit during development, since it doesn't affect whether a demo runs successfully. It's typically the first layer that matters once real, unfiltered production input starts flowing through the system.

Testing and rolling out model changes safely

Testing a computer vision system is not the same problem as testing a typical backend service, because the "correctness" of a model's output is probabilistic rather than deterministic. A few practices that hold up in production:

  • Golden datasets. Maintain a fixed, versioned set of representative frames — including edge cases like poor lighting, occlusion, and unusual angles — and run every model candidate against it before deployment. This catches regressions that a single accuracy number can hide.
  • Shadow deployment. Run a new model version alongside the current one on live traffic, log both sets of predictions, but only serve the current model's output to the application. Compare divergence before cutting over.
  • Canary rollout. Route a small percentage of streams to the new model version, watch the output-quality metrics described below, and expand gradually rather than switching all traffic at once.
  • Version everything together. Model weights, preprocessing code, and postprocessing thresholds should be versioned as a unit. A model upgraded without its matching preprocessing changes is a common, hard-to-diagnose source of silent accuracy loss.

None of this requires elaborate infrastructure to start — even a simple script that reruns the golden dataset and diffs the output against the previous version, run as a pre-deployment check, catches a meaningful share of regressions before they reach production traffic.

Monitoring a computer vision system

Standard infrastructure monitoring — CPU, memory, HTTP error rates — tells you whether the servers are healthy. It tells you almost nothing about whether the system is doing its job correctly. A computer vision pipeline needs a second layer of monitoring focused on the workload itself:

Category Example metrics
Pipeline health inference latency, end-to-end latency, queue depth, dropped frames
Throughput frames processed per second, per-stream processing rate
Reliability API/model error rate, timeout rate, retry rate
Output quality detection rate over time, confidence distribution, anomalous class frequency
Resources GPU/CPU utilization, memory, cost per stream

The output-quality category is the one teams most often skip, and it's the one that catches real problems: a camera that's drifted out of position, lighting conditions the model wasn't trained on, or a silent model regression after a deployment. None of those trigger an infrastructure alert — the servers are healthy, the API returns 200s, and the system is quietly producing wrong answers. A practical baseline is to alert on sudden shifts in detection rate or confidence distribution relative to a rolling historical average, not just on hard thresholds, since "normal" varies by time of day, camera, and scene.

Data retention, privacy, and access control

Video pipelines carry more regulatory and security weight than most backend services, and it's worth treating this as a first-class design concern rather than an afterthought bolted on before a compliance review:

  • Retention policy. Decide, before launch, how long raw frames, derived detections, and any identifying data are kept, and enforce it programmatically rather than manually. Raw video is usually the most sensitive and least necessary to retain long-term — in many pipelines, only the derived detections need to persist past a short debugging window.
  • Access control on the inference API, not just the surrounding application. An inference endpoint that accepts arbitrary uploaded frames without authentication is a real exposure, especially if it's compute-expensive and internet-reachable.
  • Data minimization. Where the use case allows it (occupancy counts, aggregate analytics), storing counts or bounding-box metadata rather than raw imagery reduces both storage cost and privacy exposure.
  • Jurisdiction-specific rules. Video analytics involving people frequently intersects with biometric and surveillance regulations that vary meaningfully by region — this is worth a specific legal review rather than a generic privacy policy, particularly for anything resembling facial recognition or persistent identity tracking.

Scaling from one stream to a hundred

The jump from one camera to ten is mostly a capacity question. The jump from ten to a hundred usually forces architectural changes: workload distribution across GPU workers, queue partitioning per stream or per region, and storage/bandwidth costs that scale linearly with stream count in a way that's easy to underestimate early on.

It's worth resisting the temptation to name specific infrastructure (a particular message broker, orchestration platform, or cloud service) unless it's actually in use — the underlying decisions (how work is distributed, how failures are isolated, how state is partitioned) matter more than the specific tools, and the right tools vary a lot by scale and existing infrastructure.

Cost tends to grow in three places that are easy to overlook during initial design: GPU idle time from poorly batched or unevenly distributed inference requests, egress and storage cost from retaining more raw video than the use case actually requires, and the operational cost of running enough redundant capacity to tolerate a single worker or region failure without dropping streams.

A practical readiness checklist

Before calling a real-time computer vision system production-ready, it's worth having explicit, testable answers — not just intentions — for each of these:

  • What's the actual end-to-end latency budget, measured stage by stage?
  • What happens when the model times out or returns malformed output?
  • What's the behavior under sustained overload — degrade gracefully, or fall over?
  • Is output quality monitored separately from infrastructure health?
  • Is there a tested rollout process for new model versions, including a rollback path?
  • What's the data retention policy, and is it enforced automatically?
  • What's the plan for scaling stream count, and where does the current architecture stop working?
  • Are security and access controls applied to both the video input and the inference API, not just the surrounding application?
  • What's the actual cost per stream at current and projected scale?

If any of these don't have a concrete answer, that's the gap to close before launch, not after.

Closing thought

None of this is specific to a single industry or use case, and that's largely the point. Whether the pipeline handles occupancy counting, defect detection, or analytics on out-of-home advertising displays — the class of problem behind platforms like Oohlytics, Macromodule's computer-vision analytics product for billboard and signage measurement — the underlying engineering challenge holds steady: a model that performs well in isolation is not the same thing as a system that performs reliably under continuous, real-world load. The model is frequently the more tractable part of the problem. The surrounding pipeline — ingestion, validation, monitoring, rollout, and data governance — is where the engineering effort actually goes, and where the difference between a demo and a product gets decided.

Top comments (0)