Milvus v3.0.1 was released on 9 September 2026 and the upgrade from 2.6 is an image tag change with no migration step, which is what makes it easy to get wrong. The vendor validated that path from Milvus 2.6.20 only, with the Helm chart held at whatever version is already installed, with MixCoord and StreamingNode and no IndexNode, and with the message queue unchanged. The 3.0.0 release note says compatibility and rollback are guaranteed; all five upgrade guides say the procedure does not validate a rollback by changing the image back to 2.6.x, and that after v3.0.x writes data an image-only rollback can fail to read the updated state. Storage V3 is the one-way door and it is off by default at common.storage.useLoonFFI, but enabling it does not close the door at once: background compaction rewrites eligible existing segments over time, so the rollback window decays instead of ending. The two index version knobs are opt-in, the release note tells you to set dataCoord.targetVecIndexVersion to 10 and dataCoord.targetScalarIndexVersion to 4, and the config file at tag v3.0.1 ships them at 8 and -1 with no entry in the configuration reference. Raising the target changes new index builds only unless you also enable forceRebuildSegmentIndex, which ships false. The CUDA 12.9 change the release note frames as a 3.0 consequence actually landed in 2.6.19. Start by restoring a staging copy of production, running the image-only upgrade against it with useLoonFFI pinned explicitly to false, and exercising your pinned SDK's real call set before anything else is touched.
Milvus v3.0.1 was released on 9 September 2026, six weeks after 3.0.0. Read the procedure for a Milvus 3.0 upgrade from 2.x and it looks trivial, and it is: the path the vendor validated changes one image tag and nothing else. No dump and reload, no offline conversion pass, no migration job.
Now the other fact. The configs/milvus.yaml shipped at tag v3.0.1 sets dataCoord.targetVecIndexVersion: 8 on line 749 and dataCoord.targetScalarIndexVersion: -1 on line 754. The 3.0.0 release note tells you to raise those same two keys to 10 and 4 before the new index algorithms take effect, and neither key has an entry in the configuration reference. Both are accurate, and together they describe the release: the upgrade is cheap, and every decision after it is a hand-set flag with thin documentation behind it.
Everything below was read against Milvus v3.0.1 and its shipped config, Milvus 2.6.23, Helm chart milvus-5.0.27 and pymilvus 3.0.1.
The upgrade is an image swap, and the validated envelope is narrow
The guides for all five deployment shapes (cluster on Helm, cluster on the Operator, standalone on Helm, standalone on the Operator, standalone on Docker Compose) describe the same operation: change the image, leave everything else alone. What varies is what "everything else" includes. Five conditions gate the procedure.
Start by writing down what you have: the chart version you must not change is a value you read out of Helm, not your repository.
# Cluster, Helm path. helm get values <release-name> \ --namespace <namespace> \ --all > milvus-values-before-upgrade.yaml kubectl get pods --namespace <namespace> # Read the CHART column, strip the 'milvus-' prefix, and use the rest as <current-chart-version>. helm list --namespace <namespace>
The upgrade pins the chart to that captured value and changes only the tag. --reset-then-reuse-values keeps your existing values while the override lands on top.
# Prerequisite per the docs: Helm 3.14.0 or later. # Keep the chart version you already have. Change only the image tag. helm upgrade <release-name> zilliztech/milvus \ --namespace <namespace> \ --version <current-chart-version> \ --set image.all.tag="v3.0.1" \ --reset-then-reuse-values \ --wait \ --timeout 30m # The standalone page is identical except for --timeout 20m.
The Operator path carries a trap of its own. Apply the complete Milvus custom resource, not an image-only merge patch, because the Operator can default omitted zero-replica component fields and re-enable a component the existing 2.6.x deployment had disabled. That is how a cluster running MixCoord with IndexNode at zero replicas acquires an IndexNode during a tag change.
# Back up the live CR first.
kubectl get milvus <instance-name> \
--namespace <namespace> \
--output yaml > milvus-before-upgrade.yaml
# Apply the COMPLETE CR, never an image-only merge patch. Excerpt showing the fields to confirm:
# apiVersion: milvus.io/v1beta1
# kind: Milvus
# metadata:
# name: <instance-name>
# namespace: <namespace>
# spec:
# components:
# image: milvusdb/milvus:v3.0.1
# indexNode:
# replicas: 0
kubectl apply --filename milvus.yaml
kubectl get milvus <instance-name> \
--namespace <namespace> \
--output jsonpath='{.status.status}{"\t"}{.status.currentImage}{"\n"}'Standalone Docker Compose follows the same rule: copy the file aside, change only the image on the standalone service to milvusdb/milvus:v3.0.1, then docker compose pull standalone and docker compose up --detach standalone.
Pick v3.0.1 as the target rather than 3.0.0. Beyond the Storage V3 and index fixes below, 3.0.1 fixed unauthenticated access through streaming gRPC calls on the external proxy port, which is the kind of version-floor argument we work through in self-hosted vector database security hardening.
| Condition | What the v3.0.x upgrade guides state |
|---|---|
| Source version | Validated from Milvus 2.6.20 to v3.0.x. The Helm path was validated with Milvus Helm Chart 5.0.22; the Operator path with Milvus Operator 1.3.0 |
| Chart version | "Do not change or downgrade the Helm Chart as part of this procedure. Keep the Chart version already installed for your Helm release." Prerequisite is Helm 3.14.0 or later |
| Component topology | The validated 2.6.20 deployment used MixCoord and StreamingNode and did not run IndexNode. "If your current values enable IndexNode or use another component topology, do not run this image-only upgrade" |
| Message queue | "you must maintain your current message queue choice. Switching between different message queue systems during the upgrade is not supported" |
| Cluster Docker Compose | Not available. The 2.6.20 and v3.0.x release assets ship Compose configurations for standalone deployments only |
The rollback guarantee and the rollback warning say different things
The two paragraphs that decide this upgrade live in different documents.
The 3.0.0 release note, under compatibility and behavior notes, states that "2.6 to 3.0 compatibility and rollback are guaranteed" and that "a 3.0 deployment can be rolled back to 2.6. However, once you enable or use features that change the serialized data format (for example Storage V3), rollback is no longer possible."
All five upgrade guides carry an identical paragraph: "This procedure does not validate a downgrade or rollback by changing the Milvus image back to 2.6.x. After v3.0.x writes data, an image-only rollback can fail to read the updated state. If the upgrade fails, stop writes and use a recovery plan that restores the pre-upgrade metadata and persistent data backups. Validate the recovery plan in a non-production environment first."
They are not contradictory, and the distinction is the safety argument. The release note is a claim about the format: 2.6 can read what an unmodified 3.0 deployment wrote. The guide is a claim about the operation, which nobody validated. A format guarantee does not make an untested procedure safe, so plan against the guide.
The 3.0.1 notes show the gap is not theoretical. One fix reads: "Fixed rebuilt or compacted nested HYBRID indexes with high-cardinality data becoming unreadable after rollback to an older version." That is the guides' failure mode, patched in the first patch release.
The second trap is circular. The 3.0.0 release note lists Snapshot among the features that depend on Storage V3, so taking a 3.0 Snapshot as pre-upgrade insurance is the act that closes the rollback door. Two vendor sources disagree: the "features that require Storage V3" table on the architecture page lists four entries (the Vortex file format, TEXT fields, Function-generated vector fields and External collections) without Snapshot, and the snapshots user guide never mentions Storage V3 or useLoonFFI. Take the stricter reading. Your insurance is an external backup of metadata and persistent data, which is what every upgrade guide asks for.
Storage V3 is a one-way door that closes gradually, not at once
Storage V3, internally called Loon, is manifest-based columnar storage on object storage, and it ships off: common.storage.useLoonFFI: false on line 1086 of the config at tag v3.0.1, with common.storage.enableGrowingSourceFlush: false beneath it and dataNode.storage.format: parquet on line 949 offering vortex as the alternative. The architecture page carries a beta marker for Milvus 3.0.x.
Two statements on that page decide whether to touch it at all. "Enable Storage V3 to use features that depend on it, rather than as a general performance optimization." And: "Once Milvus writes data in Storage V3, downgrading to a Milvus version that cannot read Storage V3 is not supported. Disabling Storage V3 later does not immediately convert all existing Storage V3 data or restore compatibility with the older version."
The mechanism behind the second changes how you plan the change window: "After common.storage.useLoonFFI takes effect, new writes and compaction output use Storage V3. Existing data remains in its current layout until eligible data is rewritten by background compaction. Milvus can read both layouts during this transition." So the door never slams. It closes segment by segment on compaction's schedule, which makes your rollback position a window narrowing at a rate nobody has published. That is the same background machinery whose limits we mapped for deletion in vector database GDPR erasure and HNSW soft deletes.
Enablement itself is three lines, plus the two compaction switches the page asks for when you add a Function and its generated vector field to an existing collection.
# Source: the Storage V3 architecture page.
# This is the one-way door. Read the rollback section of this post before applying it.
common:
storage:
useLoonFFI: true
# Only if you will add a Function and its generated vector field to an EXISTING collection:
dataCoord:
compaction:
bumpSchemaVersion:
enabled: true
storageVersion:
enabled: trueOne of those keys deserves a warning. dataCoord.compaction.storageVersion.enabled is described as enabled by default on the architecture page, and a grep of configs/milvus.yaml at tag v3.0.1 finds no storageVersion key in the file. It is documented behaviour you cannot confirm from the shipped defaults.
For a Helm deployment every key above travels the same route: chart milvus-5.0.27's values.yaml describes extraConfigFiles.user.yaml as merging into milvus.yaml at top priority over the image and chart defaults. The first value to send through it is not true but an explicit false. The release note says Storage V3 will be enabled by default in a later release, and the pin stops a future image or chart bump deciding that for you.
# Chart 5.0.27 values.yaml: this config merges into milvus.yaml and is top priority
# over the image and chart defaults. Pin the flag rather than inheriting a default.
extraConfigFiles:
user.yaml: |+
common:
storage:
useLoonFFI: falseThe two index version knobs ship at the wrong values and are absent from the reference
The 3.0.0 release note states: "New index versions are opt-in for now. Newly introduced index algorithms require manually raising the target index version (dataCoord.targetVecIndexVersion to 10, dataCoord.targetScalarIndexVersion to 4) before they take effect; a later release will enable them by default."
Here is what the server ships.
Two of those five keys are the ones you are told to change, and they are the two with no entry in the reference. A search across the English documentation set finds targetVecIndexVersion and targetScalarIndexVersion exactly once, on the release notes page. The DataCoord reference documents dataCoord.autoUpgradeSegmentIndex and says nothing about either target, so the release note is the specification.
The comment block shipped above the keys explains why raising them can change nothing. It states that if forceRebuildSegmentIndex is enabled the vector index will be rebuilt to align with targetVecIndexVersion, and that if it is not enabled the newly created vector index aligns with whichever is newer of the index engine's version and targetVecIndexVersion. The scalar comment block is the same shape. Raising the target is a decision about new builds; rebuilding what you already have is a second, larger decision.
# Shipped defaults in configs/milvus.yaml at tag v3.0.1: # dataCoord.targetVecIndexVersion: 8 # dataCoord.targetScalarIndexVersion: -1 # dataCoord.autoUpgradeSegmentIndex: false # dataCoord.forceRebuildSegmentIndex: false # dataCoord.forceRebuildScalarSegmentIndex: false # # Values the 3.0.0 release note tells you to set. Neither key has an entry in the # configuration reference, so treat the release note as the only specification. dataCoord: targetVecIndexVersion: 10 targetScalarIndexVersion: 4
One more reason to be on the patch release: v3.0.1 fixed interim indexes ignoring the configured target index version, so on 3.0.0 a correctly raised target could still be ignored.
The awkward part is observability. No documented API, CLI command or metric reports the index version in use on a segment, so you cannot confirm the change by reading a value back. Behaviour is the only observable: build one new index on staging with the target raised, leave a comparable segment untouched, and compare search latency, recall and index size. That is the evidence for or against forceRebuildSegmentIndex across the collection.
Key under dataCoord | Shipped default at v3.0.1 | Value the 3.0.0 release note names | What it governs |
|---|---|---|---|
targetVecIndexVersion | 8 | 10 | The version new vector index builds align to |
targetScalarIndexVersion | -1 | 4 | The version new scalar index builds align to |
forceRebuildSegmentIndex | false | not mentioned | Rebuilding existing vector segment indexes to the target |
forceRebuildScalarSegmentIndex | false | not mentioned | Rebuilding existing scalar segment indexes to the target |
autoUpgradeSegmentIndex | false | not mentioned | Auto upgrading segment indexes to the index engine's version |
The CUDA change landed in 2.6.19, not in 3.0
The 3.0.0 release note says "GPU images move to CUDA 12.9 and no longer preserve Ubuntu 20.04 GPU compatibility," and read alone it sounds like a consequence of upgrading to 3.0. The GPU Dockerfile at each tag says otherwise.
The move from 11.8.0 to 12.9.1 happened in Milvus 2.6.19, released 26 June 2026. A cluster on 2.6.19 or later sees no CUDA change at the 3.0 boundary. Only a cluster on 2.6.18 or earlier meets it during the jump, which argues for taking a recent 2.6 patch first and treating the driver and container toolkit work as its own change. The CPU image at v3.0.1 is built on ubuntu:jammy-20240530.
The Ubuntu 20.04 half of that release note is a vendor statement the build tree does not corroborate: build/docker/milvus/gpu/ still contains both an ubuntu20.04 and an ubuntu22.04 directory at v2.6.23 and at v3.0.1, and no ubuntu20.04-suffixed milvusdb/milvus tag is published. If your GPU hosts run 20.04, pull the image and run it rather than trusting either source.
Resist filling the gap with a driver floor. The one vendor page that names a number, the GPU prerequisites page, was last committed in June 2024, lists six supported compute capabilities (6.0, 7.0, 7.5, 8.0, 8.6 and 9.0) and recommends drivers of version 545 and above. It predates the CUDA 12.9.1 base image by two years, so it is not a 3.0 requirement, and a number reasoned from CUDA minor version compatibility is a guess wearing a citation.
| Milvus tag | First FROM in build/docker/milvus/gpu/ubuntu22.04/Dockerfile |
|---|---|
| v2.6.0 | nvidia/cuda:11.8.0-runtime-ubuntu22.04 |
| v2.6.18 | nvidia/cuda:11.8.0-runtime-ubuntu22.04 |
| v2.6.19 | nvidia/cuda:12.9.1-runtime-ubuntu22.04 |
| v2.6.20 | nvidia/cuda:12.9.1-runtime-ubuntu22.04 |
| v2.6.23 | nvidia/cuda:12.9.1-runtime-ubuntu22.04 |
| v3.0.1 | nvidia/cuda:12.9.1-runtime-ubuntu22.04 |
What you are actually upgrading for, and what it costs
A major version upgrade is justified by what it unlocks. Three of the additions in 3.0 carry documented limits sharp enough to change whether they unlock anything for you.
Five limits are named for Milvus 3.0.0 specifically. A TEXT field cannot be a primary field, partition key or clustering key. TEXT fields do not support default values. They are not supported in external collections. You cannot create a scalar index on one. And a BM25 or MinHash Function that uses a TEXT field as input must be defined when the collection is created: add_function_field and AlterCollectionSchema cannot add it later, even if the collection is empty. Get that Function wrong at creation time and the fix is a new collection.
# Source: the sparse inverted index user guide.
from pymilvus import MilvusClient
client = MilvusClient(uri="http://localhost:19530")
index_params = client.prepare_index_params()
index_params.add_index(
field_name="sparse_vector",
index_type="SPARSE_INVERTED_INDEX",
index_name="sparse_ip_index",
metric_type="IP",
params={"inverted_index_algo": "SINDI"},
)
client.create_index(
collection_name="your_collection_name",
index_params=index_params,
)
# Valid inverted_index_algo values: DAAT_MAXSCORE, DAAT_WAND, TAAT_NAIVE,
# BLOCK_MAX_MAXSCORE, BLOCK_MAX_WAND, SINDI.TEXT fields require Storage V3, so wanting them is a rollback decision
TEXT makes long text a first-class field type with text_match, phrase_match and BM25 support. The gate is unambiguous: common.storage.useLoonFFI defaults to false, and before creating a collection that contains a TEXT field you must set it to true, or Milvus rejects the collection schema. The page carries a beta marker for Milvus 3.0.x. The "64 KB" figure from the release note is a tunable, and the key is what belongs in your config review.
The FAISS adapter is a CPU passthrough with a narrow type surface
The new FAISS index type accepts a Faiss index-factory string through a required build parameter, faiss_index_name. The page carries a beta marker for Milvus 3.0.0 and later, and four of its documented limits define where it fits. The generic adapter runs on CPU and is not a Faiss GPU index type. It supports FLOAT_VECTOR and BINARY_VECTOR and does not support FLOAT16_VECTOR, BFLOAT16_VECTOR, INT8_VECTOR or SPARSE_FLOAT_VECTOR. Search iterators are not supported. And for 3.0.0 the documentation asks you to validate COSINE scores and range-search thresholds after an index reload, because Knowhere v3.0.6 does not restore the adapter's cosine-normalization state during deserialization. Seven factory strings are listed as release-tested: Flat, IVF64,Flat, HNSW16,Flat, OPQ16,IVF64,PQ16x4, IVF64,PQ8x4,RFlat, PQ8x4 and BFlat. Only the first three are covered for scalar-filtered search, and standalone PQ8x4 rejects the selector filtered search uses. Anything outside that set is your own experiment.
The sparse index has six algorithms and two stories about the default
Sparse vector search in 3.0 exposes the search algorithm as a build parameter on a SPARSE_INVERTED_INDEX. Six values are valid: DAAT_MAXSCORE, DAAT_WAND, TAAT_NAIVE, BLOCK_MAX_MAXSCORE, BLOCK_MAX_WAND and SINDI. SINDI is a sparse inverted index on fixed document-id windows with SIMD acceleration, published as SINDI: An Efficient Index for Sparse Vector Approximate Maximum Inner Product Search. Set the parameter explicitly: the two vendor sources describe the default with different preconditions. The user guide says that if you do not specify inverted_index_algo, Milvus selects on metric_type: DAAT_MAXSCORE for BM25 and SINDI for IP. The release note attaches a condition, saying that once the new index version is enabled, SINDI is the default for sparse IP search and MaxScore the default for BM25. Since that index version is the opt-in knob from the previous section, an unset parameter on a default install may give you neither. The performance claim attached to this work is a vendor internal benchmark and belongs in that exact shape: in internal benchmarks the compressed BM25 index is roughly 3x smaller than the 2.6 sparse index at comparable recall, and SINDI reaches up to about 10x the QPS of MaxScore on learned sparse embeddings. Measure both on your own corpus. Why it matters at all is the retrieval argument in hybrid dense and sparse embeddings for search.
| Key | Default at v3.0.1 | Meaning |
|---|---|---|
dataNode.text.inlineThreshold | 65536 | TEXT values smaller than this many bytes are stored inline instead of in LOB files |
dataNode.text.maxLobFileBytes | 67108864 | Maximum size of a single LOB file for TEXT column storage |
dataNode.text.flushThresholdBytes | 16777216 | Flush threshold for the TEXT column writer buffer |
dataNode.compaction.lobHoleRatioThreshold | 0.3 | If the hole ratio reaches this threshold, compaction rewrites LOB files rather than reusing them |
Rehearse the upgrade as a restore drill
The rehearsal is a dry run of the restore, with the upgrade in the middle. Restore your most recent backup into an isolated cluster, run the image-only upgrade there, then practise going back under time pressure. The backup mechanics, including how the backup version window constrains you, are in why a snapshot restore returns an empty collection.
Capture four things before the change window opens: the full Helm values or the live custom resource, the chart version and pod inventory, an external backup of metadata and persistent data, and a retrieval baseline. Verification after the swap is mechanical.
helm history <release-name> --namespace <namespace>
kubectl get pods --namespace <namespace>
kubectl get pods --namespace <namespace> \
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{range .spec.containers[*]}{.image}{" "}{end}{"\n"}{end}'Do not skip the retrieval baseline, and do not let it be QPS alone. An index version change moves recall, and recall moves hardest under metadata filters; the harness is in filtered vector search and recall collapse under selectivity. Record filtered and unfiltered recall on a fixed query set before the upgrade so the number after it means something.
For Storage V3 the observables are object storage bytes and compaction task counts on the staging copy. Nothing published quantifies how long the transition takes or how much capacity the rewrite needs at peak, so watch both from the moment useLoonFFI goes true: that curve is your shrinking rollback window.
Client SDKs get a test, not an assumption. Nothing in the 3.0.0 body, the 3.0.1 body, the five upgrade guides or the pymilvus 3.0.1 release notes states whether 2.x client calls are source-compatible with a 3.0 server, in either direction. Point your pinned client at the 3.0.1 staging cluster and run your real call set. One adjacent fix shows why: Milvus 2.6.23 fixed Milvus 2.6 accepting the unsupported Text data type, so a newer client could express a schema the server could not support.
In a private or air-gapped deployment, the rollback plan is the only plan
On client infrastructure there is no vendor-side restore point, so the recovery plan you validated is the entire safety net. Two things follow. Pin by tag in your manifests and record the digest your registry mirror resolved in that window's change record. A tag is a moving pointer: the digest milvusdb/milvus:v3.0.1 resolves to is not guaranteed to stay the same. Your mirror's copy is the artifact under change control, so the digest belongs in the ticket. Keep topology changes out of the version window. The release note describes Woodpecker, the WAL behind the streaming write path, as deployable as an independent service. Do not take that change in the same window: v3.0.1 fixed concurrent writes being lost during WAL backend migration, the message queue choice is frozen for the upgrade anyway, and a failure with two variables in flight cannot be bisected.
When staying on 2.6 is the correct answer
The 2.6 line is still shipping patches: 2.6.23 was published on 28 August 2026. Staying is a maintained position, not a frozen one. Six signals decide it.
None of those rows is about which engine to run. If the real question is whether Milvus is the right store at your scale, we ran those numbers in vector search cost per QPS at a billion vectors.
One note for anyone building fresh. The Helm procedure was validated with Chart 5.0.22 and tells you to keep whatever chart version you already have, which sidesteps the question for an in-place upgrade. Chart milvus-5.0.27, published 3 September 2026, carries appVersion: "3.0.1" and an image tag of v3.0.1, and is the newest chart available. A new install on it sits outside what the guide validated, which makes the rehearsal more important rather than less.
Whichever way that table points, the same first day applies. Restore your most recent Milvus backup into an isolated staging cluster and run the image-only upgrade to v3.0.1 against that copy, with common.storage.useLoonFFI pinned explicitly to false through extraConfigFiles.user.yaml. Then restore the same backup again, on the clock, and confirm you can get back to the 2.6 patch you started from using an external artifact rather than an image tag. Record filtered and unfiltered recall either side, and run your pinned SDK's real call set. That turns every uncertainty in this post into a measurement on your data. The wider retrieval architecture around these decisions is mapped in our RAG systems pillar.
| Signal in your deployment | What it argues |
|---|---|
| You need TEXT fields, Snapshot, External Collection or Function-generated vector fields | Move, and accept that the rollback window starts decaying once compaction runs |
| You want the new sparse or scalar index algorithms | Move, but the knobs are opt-in, ship below the values you are told to set, and are documented only in a release note |
| Your topology runs IndexNode, or you want to change the message queue or the WAL backend | Stay until that topology change has landed and settled on the 2.6 line |
| You run GPU nodes on Milvus 2.6.18 or earlier | The CUDA base image change is ahead of you either way, so take it on 2.6 first as its own change |
| You cannot produce a validated restore from an external backup | Stay. The rollback plan is the prerequisite for the upgrade, not the fallback after it |
| Your pinned SDK call set has not been exercised against a 3.0.1 server | Stay until it has, since no vendor source answers the compatibility question |
FAQ
Quick answers to the questions this post tends to raise.



