Argo CD 3.6 came out in the first week of November 2026, with a 3.6.1 patch release right behind it. It is a minor release, so there is no big migration like the move from 2.x to 3.0. Still, a few changes need attention before you upgrade a production instance. Diffing changes for every Application that uses Server-Side Apply, tracing settings have new rules, and some event messages and health statuses read differently.
This article goes through what changed, who it affects and the steps to upgrade from 3.5. It follows the official 3.5 to 3.6 upgrade notes.
Check that your clusters are supported
Argo CD 3.6 is tested against Kubernetes 1.34, 1.35, 1.36 and 1.37. Version 3.5 covered 1.33 to 1.36, so 1.33 drops off the list. If Argo CD runs on, or deploys to, a 1.33 cluster, plan that cluster upgrade first or wait with Argo CD.
kubectl version
argocd cluster list
The second command lists each managed cluster with its Kubernetes version.
Server-Side Apply now means Server-Side Diff
This is the change most teams will notice. Until now, an Application with the ServerSideApply=true sync option still worked out its diff locally, using the Structured-Merge Diff strategy. That strategy only approximated what the API server would do, and the community hit enough problems with it that 3.6 removes it.
In 3.6, those Applications use Server-Side Diff instead. Argo CD sends a Server-Side Apply dry run to the kube-apiserver and compares the result with the live object. The diff then matches what a real sync would produce, including fields the API server fills in. Changes made by mutating webhooks stay out of the diff unless you add IncludeMutationWebhook=true to the same compare-options annotation.
Server-Side Diff is now active when any one of these is true:
- the
controller.diff.server.sidesetting is enabled inargocd-cmd-params-cm - the Application has the annotation
argocd.argoproj.io/compare-options: ServerSideDiff=true - the Application uses the
ServerSideApply=truesync option, which is new in 3.6
Two side effects come with it, and neither applies to resources that don't exist yet, since Argo CD skips the dry run when a resource is being created. First, the API server gets more dry-run requests, in proportion to the number of resources in those Applications. Argo CD caches the results and only repeats the dry run on a refresh, a new Git revision, a change to the Application spec or a change to a resource's resourceVersion. On a large instance with many Server-Side Apply Applications, watch API server latency for a day after the upgrade.
Second, validating webhooks now run during the diff. A resource that a webhook would reject shows up as a diff error right away, instead of only failing when you sync. That's usually an improvement, but it can make Applications that looked fine show errors after the upgrade. Those errors were already there. They just surfaced late before.
If you need the old behavior for one Application, set the opt-out annotation. It always wins and falls back to the legacy client-side diff.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payments
namespace: argocd
annotations:
argocd.argoproj.io/compare-options: ServerSideDiff=false
OpenTelemetry tracing changes
If you don't send Argo CD traces to a collector, you can skip this section. If you do, there are four things to check.
The repo-server header variable was renamed
The repo-server now reads collector headers from ARGOCD_REPO_SERVER_OTLP_HEADERS. The old name, ARGOCD_REPO_OTLP_HEADERS, didn't follow the pattern every other component uses, and 3.6 stops reading it entirely. The default manifests already set the new name from the otlp.headers key, so only custom patches or Helm values are affected. Search your configuration for the old name.
kubectl -n argocd get deployment argocd-repo-server -o yaml | grep -n "OTLP_HEADERS"
grep -rn "ARGOCD_REPO_OTLP_HEADERS" .
A related fix makes header values that contain = work, such as base64-encoded credentials. Each pair is now split on its first =. Before 3.6, such a value made Argo CD drop every header.
otlp.insecure now decides TLS on its own
The otlp.insecure setting now fully controls whether the connection to the collector uses TLS. With the default, "true", Argo CD always connects in plaintext. Before 3.6, setting OTEL_EXPORTER_OTLP_CERTIFICATE or OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE quietly switched the connection to TLS. If you relied on that, set otlp.insecure: "false" explicitly before upgrading, or tracing will try to talk plaintext to a TLS collector.
With otlp.insecure: "false", Argo CD now also trusts the CA configured for the collector host in argocd-tls-certs-cm and honors the standard OTEL_EXPORTER_OTLP_* certificate, client key and header variables, so mutual TLS works. A component now refuses to start when those variables point to an unreadable CA or an incomplete certificate and key pair. Check the files exist before you roll out.
Sampling is parent-based
Argo CD used to record every span once tracing was on. It now uses a parent-based sampler, with the root sampling rate set per component by --otlp-sample-ratio or the matching ARGOCD_<COMPONENT>_OTLP_SAMPLE_RATIO variable. The default is 1.0, so you still get every trace unless you lower it.
The catch is upstream trace context. If a proxy or gateway in front of argocd-server sends a traceparent header marked as not sampled, Argo CD now honors that decision and records nothing, even at a ratio of 1.0. The flag rejects values outside 0 to 1 at startup. The environment variable is more forgiving. An invalid value there logs a warning and falls back to 1.0.
Span attributes changed
The gRPC and HTTP tracing libraries moved to newer OpenTelemetry semantic conventions, which renamed or dropped some span attributes. One example from the notes is server.address, which now holds the gRPC dial target instead of the peer's IP address. If Grafana dashboards or alerts filter on span attributes, test them against a 3.6 instance.
Local diffs include Helm helpers
argocd app diff --local --server-side-generate sends local files to the server for rendering. By default it used to send only *.yaml, *.yml and *.json. That left out _helpers.tpl, so charts that call named templates rendered incorrectly, and Chart.lock, so dependencies were resolved from version ranges instead of the pinned versions.
In 3.6 the default list adds *.tpl and Chart.lock. If you never pass --local-include, Helm diffs just get more accurate. If you do pass it, remember that your list replaces the defaults instead of adding to them. Repeat every pattern you want.
argocd app diff my-app \
--local ./chart \
--server-side-generate \
--local-include "*.yaml" --local-include "*.yml" --local-include "*.json" \
--local-include "*.tpl" --local-include "Chart.lock" \
--local-include "*.env"
The *.env pattern in that example covers Kustomize generators that read .env files. No default can guess such file names, so Kustomize users add them by hand.
Better health reporting
Several health changes make stuck or flapping Applications easier to explain.
When a resource is being deleted, Argo CD used to show a generic Progressing status. The default health check now lists the finalizers still pending. Custom Lua health checks can also return a deletionMessage. When a check sets it, Argo CD uses that message and the returned status for the terminating resource. That is a good place to explain what a finalizer waits for and why removing it by hand is risky.
if obj.metadata ~= nil and obj.metadata.deletionTimestamp ~= nil then
local hs = {}
hs.status = "Progressing"
hs.deletionMessage = "Waiting for the cloud load balancer to be released."
return hs
end
local hs = {}
hs.status = "Progressing"
hs.message = "Waiting for status"
if obj.status ~= nil and obj.status.ready == true then
hs.status = "Healthy"
hs.message = "Ready"
end
return hs
The event Argo CD emits when an Application's health changes now names up to three resources that caused it, with a (Caused by ...) suffix such as apps/Deployment:default/api. If a tool parses that event text, make sure it accepts the extra suffix.
There are also two built-in health check changes. GitOps Promoter's PromotionStrategy and ChangeTransferPolicy now report Healthy while a promotion is in progress, where they used to report Progressing. And CloudNativePG's Database resource gets a built-in check that only reports Healthy once the operator has applied the current generation, so sync waves can depend on it.
ApplicationSet progressive sync waits for a refresh
With progressive sync enabled, the ApplicationSet controller now adds a refresh annotation to all its Applications when one of them changes, and waits for every one to finish reconciling before it moves through the steps. That prevents decisions based on stale status. On ApplicationSets with many Applications, expect steps to start a little later than before.
Upgrade steps
Once you've handled the items above, the upgrade itself is the usual manifest apply. Read the upgrade notes for every minor version between yours and 3.6 if you skip any.
First, back up the configuration with the export command from the disaster recovery guide. Pass the namespace, because the command doesn't fail when you point it at the wrong one.
argocd admin export -n argocd > argocd-backup-$(date +%F).yaml
Then apply the 3.6.1 manifests. The --server-side --force-conflicts flags are required, because some Argo CD CRDs are too large for client-side apply.
kubectl apply -n argocd --server-side --force-conflicts \
-f https://raw.githubusercontent.com/argoproj/argo-cd/v3.6.1/manifests/install.yaml
For a high availability install, use manifests/ha/install.yaml from the same tag. The upstream notes recommend applying the full manifest set even when only image tags seem to change, because manifests can carry parameter changes too.
Wait for the components to roll out and check the version.
kubectl -n argocd rollout status statefulset/argocd-application-controller
kubectl -n argocd rollout status deployment/argocd-repo-server
kubectl -n argocd rollout status deployment/argocd-server
argocd version
Finally, look at Applications that use ServerSideApply=true. Any whose status turned Unknown, or that show a comparison error in the CONDITIONS column, are now hitting the API server dry run or a validating webhook. Read the condition message, fix the manifest or webhook, and keep the ServerSideDiff=false annotation only as a short-term escape hatch.
argocd app list
argocd app get payments