Videoflow runs one video pipeline as a DAG of independent workers
Python framework that facilitates the quick development of complex video analysis applications and other series-processing based applications in a multiprocessing environment.
At a glance
- What is it?
- Videoflow is an MIT licensed Python framework for distributed video and stream processing pipelines, where a graph of producers, processors and consumers runs as one OS subprocess per node locally or one container per node on Kubernetes, with the nodes talking to each other over a NATS JetStream broker rather than calling each other directly.
- Who is it for?
- Videoflow fits a team that has a video pipeline which has outgrown a single process, where a per node model, a per node container and a broker between the nodes is the right shape because the stages need to scale and fail independently. It does not fit a script that reads a video file and writes some detections, since the broker and the worker processes are pure overhead there, and it does not fit a deployment without Docker.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 18 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The graph is the program, the broker is the bus
Videoflow is a Python framework for building distributed video and stream processing pipelines, and the description is precise about the model. You describe your pipeline once as a directed acyclic graph of producers, processors and consumers, and Videoflow runs it as a set of independent workers that communicate over a NATS JetStream message broker.
The diagram in the README shows the shape, four nodes in a line with the broker underneath.
ββββββββββββ βββββββββββββ βββββββββββββ ββββββββββββ
β producer βββββββΆβ processor βββββββΆβ processor βββββββΆβ consumer β
ββββββββββββ βββββββββββββ βββββββββββββ ββββββββββββ
β β β β
ββββββββββββββββββββ΄βββββ NATS JetStream ββββββββββββββββ
(one stream per node)Three details in the following bullets carry the design.
Each node is identified by a stable, unique name and runs in its own worker, a subprocess locally, a pod on Kubernetes. Every node publishes its output to its own broker subject, and each node subscribes to the subjects of its real parents and reassembles its inputs, which is what makes arbitrary DAGs work, multi-parent joins, multiple independent producers, fan-out. And a node's constructor arguments must be JSON-serializable so a worker can reconstruct just its one node from configuration.
That last constraint has a consequence the README states explicitly, and it is a good design rule rather than an implementation detail. Expensive or stateful setup, opening a camera or loading a model, belongs in the node's open() method, not its __init__. The reason is that a worker is reconstructed from configuration in a fresh process, so anything expensive done in the constructor is paid for again on every restart, and anything stateful in the constructor is lost.
So the framework's central trade is this. A node does not call its neighbours. It publishes to a subject and subscribes to the subjects of its parents, and the broker holds the intermediate state. That is what lets the same graph run as subprocesses on a laptop and as pods on a cluster with per node scaling and GPU scheduling, and it is also what makes a node restart cheap, since the state lives in the broker rather than in a process.
One graph, two runtimes, no code changes
The same graph runs two ways with no code changes, and the README lists them.
Locally, as one OS subprocess per node, for fast development and testing. On Kubernetes, as one container per node, with per-node scaling, GPU scheduling, health probes and autoscaling for production.
This is the property that makes the framework worth its weight, because the alternative is a pipeline you rewrite when you move it. A local script that has the stages as function calls cannot become a distributed deployment without restructuring, and the structure you end up with after the restructuring is usually the one Videoflow imposes from the start.
The quickstart shows the shape of the code, and the flow is defined inside a build_flow() factory that returns a Flow, and the same factory is used to run locally and to deploy to Kubernetes.
from videoflow.core import Flow
from videoflow.core.constants import BATCH
from videoflow.producers import IntProducer
from videoflow.processors import IdentityProcessor, JoinerProcessor
from videoflow.consumers import CommandlineConsumer
def build_flow():
producer = IntProducer(0, 40, 0.1, name='producer')
identity = IdentityProcessor(name='identity')(producer)
identity1 = IdentityProcessor(name='identity1')(identity)
joined = JoinerProcessor(name='joined')(identity, identity1)
printer = CommandlineConsumer(name='printer')(joined)
# Producers are discovered automatically from the consumers β list only the leaves.
return Flow([printer], flow_type=BATCH)The calling convention is unusual and worth understanding. A node is called with its parents, IdentityProcessor(name='identity')(producer), and the result is the node with its upstream attached. And then the Flow is constructed from the leaves only, because producers are discovered automatically from the consumers, list only the leaves.
So the graph is assembled by chaining in reverse from the output, and the framework walks backwards to find the producers. That is unusual and it removes a whole class of bookkeeping, because the alternative is listing every node twice.
The local engine is then selected at run time.
from videoflow.engines.local import LocalProcessEngine
flow = build_flow()
flow.run(LocalProcessEngine()) # one subprocess per node, talking to NATS
flow.join()And the command line version does the same thing.
videoflow run-local my_flow.pyThe README describes what that command does in detail, and the list is a good summary of the developer experience. It generates the solution config if the graph ships a config.template.yaml, runs its prepare.py hook, starts a dev broker if none is listening, spawns one worker subprocess per node, waits for the flow to finish, reports any node that exited non-zero, and stops only the containers it started. The overrides are --nats, --config, --no-prepare, --no-infra, --keep-infra, --blob-redis-url, --blob-ttl-seconds, --run-id and --require-profile CHANNEL=PROFILE.
The install pulls a base image and starts a broker for you
The Installation section is about making the first run possible without any manual infrastructure.
It requires Python 3.12+ and docker, and a NATS JetStream server is needed at runtime, and run-local starts one for you. The package is on PyPI, and the two install forms are an environment of your own or a standalone command on the PATH.
pip install -U 'videoflow[all]' # into an environment of your own
# or, as a standalone command on your PATH:
uv tool install 'videoflow[all]'
videoflow --helpThe extras are named for what they pull in, and the naming is the documentation. The distributed extra is the broker client plus the wire format. The vision and video extras add OpenCV and ffmpeg. The deploy extra is the broker client plus oras, for publishing components as OCI artifacts. The blob extra is the Redis payload store. And there is all.
The comment in the pyproject.toml explains why the core is kept this way, and it is a good statement of the design. The core is deliberately lean, with the graph model, the serialization and the worker runtime, plus what every videoflow command reads before doing anything, YAML for solution configs, profiles, manifests and descriptors, and JSON Schema for descriptor validation. Heavy or environment-specific dependencies, the broker client, the wire format, OpenCV, Redis, oras, live in optional-dependencies so each per-component Docker image installs only what its node family needs.
So the core dependencies are numpy, six, PyYAML and jsonschema, and the actual runtime dependencies of a distributed flow arrive when you ask for them. That matters for the container images, since a detection node and a consumer node do not need the same packages, and the whole point of the OCI publishing path is that each node family gets its own image.
The infrastructure story is handled too. The videoflow-base container image that solutions build on is pulled from ghcr.io/videoflow/videoflow-base:<your version> the first time deploy or run-local needs it, and the shipped solutions are fetched on demand, and nothing is cloned or built by hand.
And you do not need to start a broker by hand, because videoflow run-local starts a dev NATS and Redis in Docker when none is already running, and stops them when the flow ends. To run one yourself instead, it will be detected and reused, either with the included docker-compose.yml or with a local binary.
docker compose up -d # NATS JetStream on :4222, Redis on :6379
# or, without Docker:
nats-server -jsThere is a subtlety about state that the README explains carefully. A dev store you run yourself keeps what a run that failed left in it, its payloads stay pinned for their readers until the TTL, 24 hours for a batch flow, so the next run starts against a fuller store. docker compose down && docker compose up -d gives you an empty one, and the dev pair run-local starts itself is removed when the flow ends, so it never carries anything over.
Four example solutions, each isolating one hard problem
The solutions/ directory holds four complete, deployable applications built from core nodes only, no models, no footage and no extra dependencies, and the README describes them as the fastest way to see the whole path actually work, and the best code to read after the README.
| Solution | Flow type | What it demonstrates | |---|---|---| | toy_calculator | BATCH | A diamond over a stream of integers: fan-out, a trace join, competing replicas, stateful aggregation, a two-parent consumer. The smallest complete solution. | | toy_router | BATCH | Partitioned parallelism: partition_by pinning each key to one replica, an async def process node, an idempotent sink. | | toy_recovery | BATCH | The error taxonomy at work: a poison message dead-lettered on first sight, a crash restarted with its un-acked messages redelivered, a self-checking recovery report. |
Each one isolates a single hard property of the model, and the choice of what to leave out is what makes them readable. Using only core nodes means there is no model to download and no video to fetch, so the thing you are looking at is the graph semantics.
toy_calculator is the baseline, and it covers the DAG features in one small graph. Fan-out is the diamond shape, two processors reading the same producer, and a two-parent consumer at the end, which is the join. A trace join is the interesting term, because it means the join is on the trace identifier rather than on the payload, so two branches of a diamond can be correlated even though their payloads differ. Competing replicas means the same node running in more than one worker with the broker deciding which one gets a message, and stateful aggregation means a node that keeps state across messages, which is the case where a node's open() method and not its constructor has to hold the state.
toy_router is about parallelism within a node. Partitioned parallelism with partition_by pins a key to a replica, so all the events for one key go to the same worker, which is what you need for a per key state machine or for ordering within a key. The async def process node is the alternative to a synchronous one, and an idempotent sink is the discipline that makes retry safe, since the broker will redeliver an unacknowledged message.
toy_recovery is the one to read if you are going to run this in production. The error taxonomy, a poison message dead-lettered on first sight, a crash restarted with its unacknowledged messages redelivered, and a self-checking recovery report that verifies the outcome. That is the failure model of a message-driven pipeline written down, and the fact that the framework ships it as an example rather than as a manual chapter says something about the priorities.
What is in the repository, and what is not
The repository top level listing is unusual and tells you about the state of the project.
There is the videoflow/ package, the docs/ directory, examples/, solutions/, k8s/, docker/, scripts/, tests/ and spec/. And then there are directories that are not code at all. There is a white-paper/ directory, a consultations/ directory, a todos/ directory, an assets/ directory, a ROADMAP.md, an HN.md, and a videoflow.code-workspace file.
So this is a project that keeps its thinking alongside its code. The white paper, the consultations, the todos and the roadmap are all in the repository, which means a reader can see the design intent and the planned direction rather than inferring them from the code. The HN.md is the Hacker News submission text, which is where the project story is told for the first time to a wider audience.
The examples/ directory is a good index of the intended uses. There is aggregators.py, custom_nodes.py, example_nodes.py, file_output.py, multicamera_time_sync.py, object_detector.py, object_tracking.py, partitioned_processing.py, reading_livestream.py, simple_example1.py, simple_example2.py, simple_mp_example1.py, simple_mp_example2.py and stopping_flow.py, plus an object_detector.ipynb and an mscoco_labels.pbtxt, which is the label map for the detection example.
The topics attached to the repository name the application areas, object-detection, object-tracking, video-analytics, video-pipeline and video-processing, and the README says it ships with off-the-shelf components for object detection, tracking, pose estimation, segmentation and video I/O, is easy to extend with your own, and can run components written in any language shipped as container images.
That last point is the one that matters for a heterogeneous team. The framework is not limited to Python components, because the unit of deployment is a container image and the unit of communication is a message on a broker subject, so a detection node in C++ and a consumer in Python compose the same graph.
Where Videoflow is the wrong choice
The first wrong case is a single-process pipeline. If your script reads a video, runs a detector and writes a CSV, then a broker, a worker process per stage and a container image per stage are all overhead, and the debugging story is worse, because the trace of what happened to a frame now goes through a message broker rather than through a stack trace.
The second wrong case is a deployment without Docker. The requirements are Python 3.12+ and docker, and the Kubernetes path is containers, so a target that cannot run containers is out.
The third wrong case is a latency sensitive path where a per frame round trip through NATS is unacceptable. The architecture is built for throughput and for stages that scale independently, and the broker hop between adjacent stages has a cost that a direct call does not. A pipeline where the whole computation takes less time than the message round trip should not be distributed.
The fourth wrong case is a team that needs deterministic ordering across the whole graph. The model gives you per node and per key ordering, through the partitioned parallelism with partition_by, but the ordering guarantees of a distributed log are not the ordering guarantees of a function call, and a stage that assumed a total order across parallel branches will need to be rewritten.
How it differs from a plain function pipeline
The comparison worth making is with writing the pipeline as Python functions that call each other, and the difference in approach is where the state lives and what a failure costs.
A function pipeline holds its state in the process. The detector returns detections, the tracker consumes them, the writer writes them, and the state moves down the call stack. It is easy to write, easy to read, and trivially parallel by splitting the input into chunks, but the chunks are independent pipelines, and a stage that must see all the data cannot be split.
Videoflow moves the state out of the process and into the broker. Each node's output is published to its own subject, each node subscribes to its parents' subjects, and the intermediate results live in JetStream. The consequence is that the graph is not a call chain but a dataflow, and a node can have several parents, and a producer can feed several consumers, and the two independent branches of a diamond can be joined later.
The second difference is failure. In a function pipeline, an exception unwinds the stack and the whole thing stops, and the cost of restarting is the cost of restarting everything. In a message-driven pipeline, a node that crashes restarts, and the messages it did not acknowledge are redelivered, and a message that is poison is dead-lettered on first sight. The toy_recovery solution exists to demonstrate exactly this, with a self-checking recovery report, and it is the clearest argument for the architecture.
The third difference is scaling. A function pipeline scales by process, which means the whole pipeline gets more resources. A graph of nodes scales by node, so the detection stage gets a GPU and the writer does not, and the framework's Kubernetes path is built around per node resources, health probes and autoscaling.
The fourth difference is the cost of that flexibility. A broker hop between two stages that could have been a function call, a serialisation format on every message, a worker process per stage, and the debugging work of following a frame across a log. For a pipeline with many stages, many inputs or a real scaling requirement, that cost buys something, and for a small one it does not.
Editorial conclusion
Videoflow fits a team that has a video pipeline which has outgrown a single process, where a per node model, a per node container and a broker between the nodes is the right shape because the stages need to scale and fail independently. It does not fit a script that reads a video file and writes some detections, since the broker and the worker processes are pure overhead there, and it does not fit a deployment without Docker. Before adopting it, read the toy_calculator and toy_recovery solutions, which are the fastest way to see the config, the prep hook, the image, the broker, the workers and the teardown work, and note that the current release is v1.0.4 from 2026-09-21 with the last push on 2026-10-07.
Frequently asked questions
What is Videoflow and how does it run a pipeline?
It is a MIT licensed Python framework for distributed video and stream processing pipelines. You describe the pipeline as a directed acyclic graph of producers, processors and consumers, and it runs as independent workers that communicate over a NATS JetStream broker, one OS subprocess per node locally or one container per node on Kubernetes.
What do I need to install Videoflow and how do I start the broker?
Python 3.12+ and docker, with a NATS JetStream server needed at runtime. The install is pip install -U 'videoflow[all]' or uv tool install 'videoflow[all]', and you do not need to start a broker by hand because videoflow run-local starts a dev NATS and Redis in Docker when none is running and stops them when the flow ends.
How do I write a Videoflow pipeline?
Inside a build_flow() factory that returns a Flow. You chain nodes by calling each one with its parents, for example IdentityProcessor(name='identity')(producer), and you pass only the leaves to Flow, because producers are discovered automatically from the consumers. The same factory is used both for local runs and for Kubernetes deployment.
What do the Videoflow example solutions demonstrate?
Four complete applications built from core nodes only. toy_calculator shows a diamond over a stream of integers with fan-out, a trace join, competing replicas, stateful aggregation and a two-parent consumer. toy_router shows partitioned parallelism with partition_by, an async def process node and an idempotent sink. toy_recovery shows a poison message dead-lettered, a crash restarted with redelivered messages and a self-checking recovery report.
Can Videoflow run components written in other languages?
Yes. The unit of deployment is a container image and the unit of communication is a message on a broker subject, so the README says components written in any language can be shipped as container images. The deploy extra adds oras for publishing components as OCI artifacts, and the base image is pulled from ghcr.io.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/videoflow-videoflow)Was this useful?