# Argo CD 3.6: what changes and how to upgrade from 3.5

> Argo CD 3.6 switches Server-Side Apply apps to Server-Side Diff, reworks OpenTelemetry tracing and improves health checks. What breaks and how to upgrade.

Published: 2026-11-05
Author: k8wiz Team
URL: https://k8wiz.com/articles/argo-cd-3-6-what-changes-and-how-to-upgrade-from-3-5

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.

```bash
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.side` setting is enabled in `argocd-cmd-params-cm`
- the Application has the annotation `argocd.argoproj.io/compare-options: ServerSideDiff=true`
- the Application uses the `ServerSideApply=true` sync 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.

```yaml
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.

```bash
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.

```bash
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.

```lua
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.

```bash
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.

```bash
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.

```bash
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.

```bash
argocd app list
argocd app get payments
```
