Customer Story How Bitso cut change failure rate 83% while scaling delivery with coding agents

ArgoCD Preview Environments: How the ApplicationSet Pattern Works and Where It Strains

ArgoCD preview environments give every open pull request its own running copy of the application. The standard way to build them is an ApplicationSet with the pull request generator: it watches your repository and stamps out a full Argo CD Application for every open PR, deployed into its own namespace and exposed at its own URL. The pattern is GitOps-native, widely used, and worth understanding in detail, including the points where it starts to strain.

This article walks through the mechanics first, then the failure modes, then the alternative architecture, request routing on a shared cluster, for teams whose service count or PR volume has outgrown per-PR duplication.

The pattern: one Application per open pull request

The pull request generator polls your source control provider and produces one set of template parameters per open PR: the PR number, the branch, and the head commit SHA. The ApplicationSet controller renders an Application from each, and Argo CD deploys it like any other. GitHub and GitLab are the common providers, with GitLab configuration keyed by project rather than owner and repository, and label filters let you gate previews to PRs that opt in with a preview label.

Configure webhooks early. The generator’s default is polling on an interval, which means a preview can lag a pushed commit by minutes. Source control webhooks trigger the ApplicationSet on push, so previews create and update within seconds of the commit.

ReviewerPull requestApplicationSetcontrollerApplicationpreview-123Namespacepr-123full stackpreview URLAuto-pruned on PR close
Each open PR gets its own Application, namespace, and preview URL, all removed when the PR closes.
Pull requestApplicationSetcontrollerApplicationpreview-123Namespacepr-123full stackpreview URLReviewerAuto-prunedon PR close
Each open PR gets its own Application, namespace, and preview URL, all removed when the PR closes.

A trimmed example shows the moving parts:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: pr-previews
spec:
  generators:
    - pullRequest:
        github:
          owner: your-org
          repo: your-service
          # tokenRef is required for private repos. Anonymous access
          # only sees public repos and has a lower rate limit.
        requeueAfterSeconds: 120
  template:
    metadata:
      name: preview-{{number}}
    spec:
      project: default
      source:
        repoURL: https://github.com/your-org/your-service
        targetRevision: "{{head_sha}}"
        path: deploy/chart
        helm:
          parameters:
            - name: image.tag
              value: "{{head_sha}}"
            - name: ingress.host
              value: pr-{{number}}.preview.example.com
      destination:
        namespace: pr-{{number}}
        server: https://kubernetes.default.svc
      syncPolicy:
        automated:
          prune: true
        syncOptions:
          - CreateNamespace=true

Per-PR namespaces and Helm overrides

Each Application targets a namespace derived from the PR number, so environments cannot collide. Helm value overrides do the per-PR specialization: the image tag pins to the head SHA, the ingress hostname embeds the PR number, and resource requests and replica counts shrink to preview scale. Teams on Kustomize do the same with per-PR overlays.

DNS, ingress, and the preview URL

A wildcard DNS record for *.preview.example.com points at the cluster’s ingress, and each preview’s ingress rule claims its own hostname. ExternalDNS can manage the records automatically, and a wildcard TLS certificate keeps the URLs presentable to stakeholders. The result is the per-PR link that gets posted back to the pull request by CI.

Cleanup

When a PR closes or merges, the generator stops emitting its parameters, the controller deletes the Application, and automated pruning removes the namespace. Two backstops are worth adding: a time-to-live sweep for environments that leak past the prune, and explicit handling for persistent volume claims, which otherwise outlive the namespace’s intent and keep billing.

Practices that keep the fleet healthy

A few conventions keep a preview fleet manageable:

  • Apply a resource quota to every preview namespace, so one runaway PR cannot starve the rest.
  • Label everything with the PR number and team, which is what makes the preview line item attributable when the cluster bill needs explaining.
  • In monorepos, filter the generator by changed path so a docs-only PR does not deploy the stack.
  • Keep preview Helm values in the same file lineage as production values, overriding the minimum, because every hand-copied value is future drift.
  • Treat secrets deliberately, since stamping production credentials into short-lived namespaces per PR widens the blast radius of every leak.

Where the pattern strains

The mechanics above are sound, and for a single service or a small stack they hold up indefinitely. The strain shows up with system size and PR volume, on several fronts at once.

Spin-up time grows with the stack. A preview that needs twenty services, migrations, and seed data takes minutes to become clickable, and slow previews stop being consulted. Data seeding is its own tax, since every namespace starts empty and realistic fixtures age badly.

Cost scales with the number of services in the stack multiplied by the number of open PRs. Every open pull request holds a complete copy of every service, mostly idle, and preview fleets routinely become one of the larger line items in a cluster bill. Reduced replica counts soften this but do not change the shape of the curve.

Operational overhead scales the same way, because every preview namespace is an environment the platform team is on the hook for: its seed data, its config currency, and the question of why it came up broken. Cluster capacity becomes a scheduling problem of its own, since preview load is spiky, quiet at night and heavy before a release, so teams either overprovision nodes for the peak or bolt on autoscaling and accept slower previews at the spike. On managed clusters like Amazon Elastic Kubernetes Service (EKS) this turns into node group and autoscaler tuning that exists only to serve previews.

Two less visible problems compound over time. Preview Helm values drift from production values, so previews slowly stop predicting production behavior. And third-party dependencies, payment sandboxes, external APIs, message brokers, either get mocked per namespace or shared with no isolation at all.

Coding agents amplify all of this. A team whose PR volume multiplies finds the duplication model multiplying its spin-up queues, its bill, and its fleet of environments to keep alive.

The alternative: route requests instead of duplicating the stack

The other architecture keeps one stable environment running, continuously deployed by Argo CD exactly as today, and deploys only the changed service per PR, as an isolated version beside the stable one. Requests carrying that PR’s routing key in a header are steered to the changed version at the sidecar or mesh layer, and every other request flows through stable services.

The approach is GitOps-compatible rather than GitOps-replacing. Argo CD keeps owning the stable environment, and the preview layer operates on top of it, which also means previews exercise the real, current versions of every dependency instead of a per-namespace copy that seeded an hour ago.

Signadot implements this model with Sandboxes, lightweight ephemeral environments created per pull request by CI, each holding the changed service inside the shared cluster and exposed at a hosted preview URL that injects the routing key. Route groups combine several changed services under one key when a change spans repositories.

The first difference a team feels is developer experience. A preview is ready as soon as one container starts, in seconds rather than the minutes a full-stack deploy and seed takes, and it stays that fast whether the stack has five services or three hundred, because nothing else needs to be deployed. Fast previews are the ones reviewers and product managers actually click on every PR, instead of saving them for the changes that seem risky.

Operational overhead drops with it. The platform team runs one baseline environment, kept in sync by Argo CD, instead of a fleet of per-PR copies that each need seed data, current config, and an answer when they come up broken. Capacity planning stops being a preview-specific problem, since the marginal preview is one or two pods rather than a namespace full of them.

Cost follows the same shape. Each preview costs one or two service instances instead of a full system copy, so the bill scales with the size of the change, not the size of the stack. That economics is also what lets the model absorb agent-amplified PR volume: when coding agents multiply the number of open changes, previews that start in seconds and cost a single service multiply with them, without multiplying the cluster.

Shared clustersyncscreatesArgoCDpreview URLpull requestservice-1service-1bSANDBOXservice-3dbtagged requestshared dependencies
ArgoCD keeps syncing the shared cluster of stable services. The Sandbox holds only the change, and the preview URL's tagged requests route through it.
syncsArgoCDpreview URLpull requestservice-1service-1bSANDBOXservice-3dbtagged requestshared dependencies
ArgoCD keeps syncing the shared cluster of stable services. The Sandbox holds only the change, and the preview URL's tagged requests route through it.

Three things have to be in place for this to work. Services must propagate trace context headers across hops, stateful resources like databases and queues need explicit per-resource isolation, and a platform component runs in the cluster. The preview environments solution page covers the managed version of the workflow.

Keep Argo CD, add previews that start in seconds

Signadot gives each pull request a hosted preview URL backed by a lightweight sandbox on the shared cluster Argo CD already syncs, so a preview costs one service instead of a stack copy. The free tier is open to every developer.

Start for free

When each approach fits

ApplicationSet: duplicate per PRRequest routing: only what changedpr-101full stackpr-102full stackpr-103full stackpr-1011 servicepr-1022 servicespr-1031 serviceCost: services x PRsCost: one or two services per PR
ApplicationSet cost grows with every service and every open PR. Request routing costs one or two services per PR.
ApplicationSet: duplicate per PRpr-101full stackpr-102full stackpr-103full stackCost: services x PRsRequest routing: only what changedpr-1011 servicepr-1022 servicespr-1031 serviceCost: one or two services per PR
ApplicationSet cost grows with every service and every open PR. Request routing costs one or two services per PR.

The ApplicationSet pattern fits:

  • stacks small enough to duplicate comfortably
  • previews that must exercise cluster-level changes, such as operators, ingress, or the manifests themselves
  • teams that want the whole mechanism in open source tools they already run

It is the right default for a handful of services and moderate PR volume.

Request routing on a shared cluster fits:

  • large service graphs, where a full copy per PR is slow to start and expensive to hold
  • teams that want previews ready in seconds, so reviewers and stakeholders keep clicking them
  • platform teams that want one environment to operate instead of a fleet
  • high or agent-amplified PR volume

The two are not mutually exclusive, and mature platform teams often run both. Application code changes, the overwhelming majority of PRs, get routed previews on the shared cluster, while the occasional PR that changes manifests, operators, or the ingress layer gets a full ApplicationSet preview, since cluster-level changes are exactly what request routing cannot isolate. Gating the expensive path behind a preview label keeps the default cheap.

The preview environments guide compares these architectures alongside vCluster and Okteto, and Per-PR Preview URLs, Explained covers the URL pattern itself, including access, data, and cleanup. For the routed model end to end, see this walkthrough of cost-efficient preview environments.

Frequently asked questions

How do ArgoCD preview environments work?

An ApplicationSet with the pull request generator polls your source control provider and emits one set of template parameters per open PR. The controller renders an Argo CD Application from each, deploying that branch's code into a per-PR namespace behind a per-PR URL, and prunes the Application and namespace when the PR closes or merges.

Where does the ApplicationSet preview pattern strain?

Spin-up takes minutes because the full stack must deploy, migrate, and seed before the preview is clickable, cost scales with services multiplied by open PRs, and the platform team ends up operating a fleet of environment copies. All three compound as the service graph and PR volume grow, especially once coding agents multiply the number of open PRs.

What is the alternative to duplicating an environment per pull request?

Request routing on a shared cluster: one stable environment stays continuously deployed by Argo CD, each PR deploys only its changed services, and requests carrying the PR's routing key are steered to them. Previews are ready in seconds, the platform team operates a single environment instead of a fleet, each preview costs one or two service instances, and the model absorbs agent-amplified PR volume.

Can I use request routing without giving up Argo CD?

Yes. Argo CD keeps syncing the stable environment exactly as before, and the routing layer operates on top of it. Mature platform teams often run both: routed previews for application code changes, which are the overwhelming majority of PRs, and a full ApplicationSet preview for the occasional PR that changes manifests, operators, or ingress.

Stay in the loop

Get the latest updates from Signadot

Validate code as fast as agents write it.