MLflow describes itself on its README as “The Open Source AI Engineering Platform for Agents, LLMs & Models,” and the claim is not marketing fluff: the project reports over 60 million monthly downloads, and the repository is one of the largest Python codebases in the ML tooling ecosystem. Most developers meet MLflow through three lines of code — start_run, log_param, log_metric — and never think about what happens after the function returns. That gap between the friendly API and the machinery underneath is exactly what this source tour covers. We cloned the mlflow/mlflow repository at master, read the tracking, storage, and model-packaging layers, and mapped the real call routes into two architecture diagrams that link every node to the actual source file on GitHub.

The interesting engineering story here is the layering. MLflow exposes a thin fluent module (mlflow/tracking/fluent.py) that keeps global state for your script, a full client class (MlflowClient) for programmatic use, an internal service client that owns the backend abstraction, and then two completely different storage backends — a plain filesystem store and a SQLAlchemy-backed database store — behind a tiny URI dispatch function. On top of all of that sit “flavors” like mlflow.sklearn and the generic pyfunc loader, plus a model registry with its own storage layer. Because every layer is a separate file with a clear seam, reading the source is genuinely pleasant, and the same seams are where you would intervene if you wanted to add a custom backend or a new flavor.

The diagram below is the 10,000-foot view: your script and the CLI on the left, the service and REST server in the middle, the stores and entities at the bottom right, and the model packaging path on the right edge. Each node is clickable and opens the exact file in the GitHub repository at the master branch, so you can treat this page as a navigable index into the codebase rather than a static picture.

MLflow architecture overview: fluent API, MlflowClient, tracking service, FileStore, SqlAlchemyStore, model registry, flavors, and the REST server

MLflow overview — the fluent facade, the client layer, the store dispatch, both backend stores, the registry, and the model packaging path. Every node links to its source file at master.

Reading the overview from left to right:

From Install to Your First Model

Everything in this section is straight from the README’s three-step quickstart and the fluent module itself. Install first:

pip install mlflow

Start the server exactly as the README suggests (uvx mlflow server is the zero-install variant; with pip you can run mlflow server from the same command class):

mlflow server

Then point your script at it and turn on autologging — the README example uses mlflow.openai.autolog(), and equivalent autolog functions exist for other frameworks. If you prefer the explicit, framework-neutral form, the fluent API is three calls:

import mlflow

mlflow.set_tracking_uri("http://localhost:5000")
mlflow.set_experiment("first-steps")

with mlflow.start_run(run_name="hello-mlflow") as run:
    mlflow.log_param("learning_rate", 0.01)
    mlflow.log_metric("accuracy", 0.93, step=1)

print(run.info.run_id)

What happens under the hood when that with block exits? start_run resolves an experiment, asks the client for a run, and stores the resulting ActiveRun as the global current run. log_param and log_metric look up that active run and route through MlflowClient into the service client. When the context manager exits, end_run calls set_terminated (L788), which stamps the run’s end time and status. If you skipped the server entirely and never called set_tracking_uri, the URI resolver falls back to the local FileStore — the README promises “no complex setup,” and the code honors that by making the filesystem backend the default.

Packaging a trained model is the second half of the story. Calling mlflow.sklearn.log_model(model, "model") inside a run does three things: it constructs a Model object holding the flavor, signature, and the run ID that produced it (see self.run_id at L428); it writes the estimator plus an MLmodel descriptor as artifacts via fluent.log_artifact, L1637; and if you passed registered_model_name, it asks the registry to pin that artifact as a new version. Loading it back is symmetric: mlflow.pyfunc.load_model, L1081 reads the MLmodel file, picks the right loader, and hands you a callable, while mlflow.sklearn.load_model, L741 delegates to pyfunc and restores the original estimator object.

Inside the Client Layer

The split between fluent.py and client.py is deliberate and worth copying in your own designs. The fluent module owns conversation state — which experiment is active, which run is current — while MlflowClient is stateless from the caller’s perspective. Both funnel into TrackingServiceClient, but they reach it differently: the fluent module lazily constructs its client on first use (the import of MlflowClient sits at L104 of fluent.py), while MlflowClient creates its service client in the constructor. The batch entry point MlflowClient.log_batch, L2733 is what autologgers call: metrics, params, and tags go up in one request instead of dozens.

The URI dispatch in utils.py is the seam that makes all backends interchangeable. set_tracking_uri stores the string; _get_store consults a TrackingStoreRegistry (defined around L236) that maps URI schemes to factory functions — empty and file: schemes go to FileStore, database schemes go to SqlAlchemyStore, and additional schemes are registered for REST, Databricks, and Unity Catalog endpoints. Registering a new backend is a one-line registry call, which is why the ecosystem has been able to add remote backends without touching the fluent layer at all.

Backend Stores: Two Implementations, One Contract

Both stores extend the same abstract contract, and the entities they exchange are plain Python dataclasses in mlflow/entities: Run, L14, RunInfo, L34, Metric, L12, and Param, L9. SqlAlchemyStore.create_run inserts a row and stamps the RunInfo status fields; log_metric (L1391) upserts into a metric history keyed by run, key, step, and timestamp so that series grow append-only; log_batch wraps metrics, params, and tags in a single managed session. Internal helpers like _get_run, L1127 enforce run existence and active-state checks before writes.

FileStore is the honest-to-goodness reference implementation: the same operations produce the same entity objects, but they materialize as a directory tree of YAML files. That is what makes mlruns/ folders so easy to inspect by hand — there is no magic, just the contract expressed as text. The cost is concurrency: the file backend is single-writer by nature, which is precisely why the server path routes everything through the database store once you graduate from a laptop.

Models, Flavors, and the Registry

The detail diagram zooms into the packaging path. A flavor is just a module that knows how to serialize one framework’s objects and register a matching loader; mlflow/sklearn/__init__.py shows the pattern end to end with log_model at L394 and load_model at L741. The base class for custom Python inference code is PythonModel, L264 in mlflow/pyfunc/model.py, paired with a PythonModelContext, L405 that hands your code the artifacts directory at load time. Because every flavor is also loadable through the generic pyfunc loader, a model written once can be served by anything that understands MLmodel — including the server routes built in mlflow/server/__init__.py.

The registry has its own storage module under mlflow/store/model_registry/, with create_registered_model, L380 and create_model_version, L982 in the SQLAlchemy implementation. A model version is essentially a pointer: it records the name, the version number, and the run_id it came from, so lineage from experiment to promotable artifact is a database join, not a convention. For teams that only need experiment tracking, the registry stays dormant; for teams promoting models across environments, it is the piece that turns “the artifact in run abc123” into “champion@v7.”

Honest Limits

A few things we deliberately did not claim. First, we did not benchmark anything: the 60-million-downloads figure is quoted from the README, and no performance numbers in this post come from our own measurements. Second, MLflow is far bigger than what fits in two diagrams — the tree lists tracing, evaluation, prompt management, an AI gateway, and deployment tooling alongside tracking, and we only toured the tracking-plus-models core that most source readers start with. Third, the backend landscape extends beyond the two stores we mapped: the dispatch registry also wires REST, Databricks, and Unity Catalog stores, which we mention but did not walk through. Fourth, MLflow Projects (mlflow.projects.run at L209) can reproduce an entire script from a MLproject file, and we link it for completeness without exploring its execution machinery. Finally, tags like mlflow.runName (defined, L11) are the glue between UI and API, but the UI’s own frontend lives outside the Python package we checked out.

If you are evaluating this stack against alternatives, it helps to read sibling tours: our Langfuse source tour covers LLM-native observability and tracing, TorchServe and TensorFlow Serving cover the serving half of the story that MLflow leaves to integrations, and Streamlit is the quickest way to build a review UI on top of logged runs. Between those and this tour you will have covered the full “train, track, package, serve” arc.

Watch PyShine on YouTube

Contents