What are ArgoCD sync waves and hooks — how do you control deployment order?
Quick Answer
Sync waves are integer annotations on Kubernetes resources that control the order in which ArgoCD applies them during a sync operation, while hooks are special resources that run at specific phases of the sync lifecycle such as PreSync, Sync, PostSync, and SyncFail, enabling ordered deployments like running database migrations before application rollouts.
Detailed Answer
Think of sync waves and hooks like staging a theater production. The stage crew sets up the backdrop first (wave -1 for config maps and secrets), then the orchestra pit is prepared (wave 0 for database migrations via a PreSync hook), the actors take their positions (wave 1 for the main deployments), and finally the curtain call sequence runs (wave 2 PostSync hooks for smoke tests and notifications). Each phase must complete successfully before the next begins, and if anything fails — say a prop malfunctions — there is a SyncFail hook that triggers the stage manager's emergency protocol. This ordered execution ensures that dependencies are satisfied before dependents are deployed.
Sync waves work through the argocd.argoproj.io/sync-wave annotation, which accepts an integer value. During a sync operation, ArgoCD groups all resources by their wave number and processes them in ascending order. Resources without a wave annotation default to wave 0. Within each wave, ArgoCD applies all resources simultaneously and waits for every resource in that wave to reach a healthy state before progressing to the next wave. If any resource in a wave fails to become healthy within the configured timeout, the sync operation is marked as failed and subsequent waves are not executed. Hooks, on the other hand, are resources annotated with argocd.argoproj.io/hook and are executed at specific phases: PreSync runs before any wave begins, Sync runs alongside wave 0, PostSync runs after all waves complete successfully, and SyncFail runs only when a sync operation fails.
Internally, the sync engine within the Application Controller orchestrates this process. When a sync is triggered, the controller first identifies all hook and wave annotations across the managed resources. It constructs an execution plan that orders PreSync hooks first, followed by each wave in numerical order with their contained resources, and finally PostSync hooks. For each phase, the controller applies the resources to the cluster and then enters a polling loop, checking the health status of each resource against ArgoCD's built-in health assessment logic. For Deployments, health means all replicas are available. For Jobs used as hooks, health means the Job completed successfully. The controller uses the argocd.argoproj.io/hook-delete-policy annotation to determine whether hook resources should be cleaned up before each sync, after successful completion, or after failure, preventing accumulation of completed Job pods.
In production, a typical deployment of the order-service might use wave -2 for namespace and RBAC resources, wave -1 for ConfigMaps and Secrets, a PreSync hook Job for database schema migrations, wave 0 for the core Deployment and Service, wave 1 for HorizontalPodAutoscaler and PodDisruptionBudget, and a PostSync hook for integration smoke tests that verify the API is responding correctly. This ensures the database schema is ready before the application starts, and the application is running before autoscaling policies are applied. Teams often combine waves with health checks on custom resources to gate progression — for example, waiting for a CertificateRequest to be issued before deploying the Ingress that references it.
The most dangerous gotcha with sync waves is circular health dependencies. If a resource in wave 0 depends on a resource in wave 1 to become healthy — for example, a Service in wave 0 that needs endpoints from a Deployment in wave 1 — the sync will deadlock because wave 0 will never become healthy and wave 1 will never execute. Engineers must carefully map dependency graphs before assigning wave numbers. Another common mistake is forgetting the hook-delete-policy annotation on PreSync Jobs; without it, the Job from the previous sync still exists and ArgoCD will not re-run it because Kubernetes prevents creating a Job with the same name. Setting HookSucceeded as the delete policy ensures the previous Job is cleaned up before the next sync creates a new one.