Library
9 min read

CI vs continuous delivery vs continuous deployment

Published

Picture a team that merges everyone's work once a month. Merge day takes three days. Half the conflicts are in code nobody remembers writing, and the release that follows breaks in ways no single change explains. Now picture a team that merges every few hours and ships to production twenty times a day. The second team has fewer outages, not more, because every change is small enough to understand and to undo.

The three terms describe how far a team has moved towards the second picture:

  • Continuous integration (CI): everyone merges into the main branch at least once a day, and every push is built and tested automatically. A red build is fixed first, before new work.
  • Continuous delivery: on top of CI, every change that passes the pipeline is ready to release. Going to production is a button anyone on the team can press, at any time.
  • Continuous deployment: there is no button. Every change that passes goes to production automatically, usually gradually and behind feature flags.

So: CI keeps the code working. Continuous delivery keeps it releasable. Continuous deployment releases it. "CD" can mean either of the last two, so ask which one people mean.

Your answer

Your team's CI takes 35 minutes per pull request and people are batching changes into big pull requests to avoid waiting. What would you change first, and how would you know it worked?

Where the ideas came from#

Grady Booch used the phrase continuous integration in 1991, and Extreme Programming made it a daily practice in the late 1990s. Martin Fowler's article on it, first written in 2000, is still the standard reference. Jez Humble and David Farley's 2010 book Continuous Delivery added the second step: treat the release itself as a boring, automated, repeatable process, so it can happen whenever the business wants.

The pipeline, and why speed matters#

A pipeline is the list of jobs that run on every push: install, lint, type check, tests, build, deploy a preview. Its most important number is how long the author waits for an answer. Under about ten minutes, people wait for it. Over twenty, they switch tasks, come back later, and start batching changes to wait less often, which brings back exactly the big risky merges CI was meant to remove.

Try the three changes teams usually make first.

Make this pull request's CI faster
1
Install dependencies
Lint and type check
Unit tests
Integration tests
Build
Deploy preview
End-to-end tests
Time until the author knows26.5 min
26.5 min per push. An author waiting that long switches to something else, and comes back an hour later. Across a day of small pull requests, that is how a team quietly stops making small pull requests.
Durations are an example for a mid sized TypeScript service. Every CI tool shows the real ones for your pipeline; start with the longest bar.

Beyond those three:

  • Run only what changed. In a monorepo, Nx and Turborepo work out which packages a change affects and skip the rest, and remote caching reuses results another machine already computed.
  • Fail fast. Put lint and type checks where they finish first, so a typo fails in one minute, not twenty.
  • Keep the main branch green. A merge queue (built into GitHub since 2023, and in GitLab as merge trains) tests each pull request on top of everything queued before it, so two changes that pass alone but break together never both land.

How teams set it up#

This is a typical GitHub Actions workflow for a TypeScript service. Jobs in the matrix run in parallel, npm downloads are cached, and the browser tests are split across four machines.

yaml
name: ci
on: [pull_request, merge_group]   # merge_group runs it again inside the merge queue
 
jobs:
  checks:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        task: [lint, typecheck, test:unit, test:integration]   # four jobs at once
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 24, cache: npm }
      - run: npm ci
      - run: npm run ${{ matrix.task }}   # integration tests start Postgres with Testcontainers
 
  e2e:
    runs-on: ubuntu-latest
    strategy:
      matrix: { shard: [1, 2, 3, 4] }
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 24, cache: npm }
      - run: npm ci && npx playwright install --with-deps
      - run: npx playwright test --shard=${{ matrix.shard }}/4

Around the workflow, four settings do as much work as the YAML:

  1. Branch rules on main: nobody pushes directly, every change needs a pull request, a review and green required checks.
  2. A preview deployment per pull request (Vercel, Netlify and most platforms do this), so a reviewer in another time zone can click the change instead of imagining it.
  3. Dependency updates as small automatic pull requests from Dependabot or Renovate, which CI then tests like any other change.
  4. No long lived cloud keys in CI. GitHub Actions, GitLab and others can log in to AWS, GCP and Azure with short lived OIDC tokens, so there is no secret to leak.
Your pipeline is part of your attack surface

In March 2025 the popular tj-actions/changed-files action was compromised: its version tags were moved to a malicious commit that printed CI secrets into build logs, in thousands of repositories that used it. A tag like @v4 can be moved; a full commit SHA cannot. Pin third party actions to a SHA (Renovate and Dependabot can keep the pins up to date), and give each workflow only the permissions it needs.

Names you will hear
  • GitHub ActionsCI built into GitHub. The default for most new projects
  • GitLabGitLab CI/CD, with merge trains
  • CircleCIHosted CI with strong caching and parallelism
  • BuildkiteRuns on your own machines, popular at large scale
  • ArgoArgo CD: GitOps deploys to Kubernetes from a repo
  • VercelPreview deployment for every pull request
  • RenovateAutomatic dependency update pull requests. Dependabot is GitHub's own
  • CodecovCoverage reports on each pull request. SonarQube adds static analysis

Measuring it: the DORA metrics#

The DORA research program, now run by Google Cloud, has surveyed tens of thousands of engineers since 2014. It found that four numbers separate strong delivery teams from weak ones, and that speed and stability go together rather than against each other:

  • Deployment frequency: how often you ship to production.
  • Lead time for changes: from commit to running in production.
  • Change failure rate: how many deploys cause a problem that needs fixing.
  • Recovery time: how long it takes to restore service when one does.

The first two are speed, the last two are stability. Interviewers like this list because it shows you think about delivery as something to measure, not a matter of opinion.

ChoiceWhat you gainWhat you payPick it when
Continuous integrationSmall merges, conflicts found within hours, and a main branch that always builds.Needs a fast, trusted test suite and the habit of fixing red builds first.Every team, always. It is the floor, not an advanced practice.
Continuous deliveryReleasing is boring and possible at any moment, so fixes reach users in minutes.Needs automated deploys, safe database migrations and rollbacks that work.Most web services and APIs. A human still chooses when to release.
Continuous deploymentThe shortest lead time: merged code is live within the hour, with no release queue.Needs feature flags, canary releases, good monitoring and automatic rollback.Mature teams with strong tests and observability. Less common for mobile apps, which go through app store review.
CI, delivery or deployment?
Every merge to main is built, tested and then waits for a release manager to press deploy.
Every merge to main goes to 5% of production traffic, then 100% if metrics hold.
Tests run on every pull request, but releases are cut every two weeks by hand.
A nightly build runs the tests on whatever is in main.

In the interview#

Interviewer
Walk me through what happens between a developer pushing code and it running in production.
You
The push opens a pull request, and CI runs lint, type checks, unit and integration tests in parallel, then builds and deploys a preview where the browser tests run. A reviewer approves, the change joins the merge queue, which tests it on top of everything ahead of it, and it lands on main. Main builds one artifact, deploys it to staging, then to production as a canary on a few percent of traffic. If error rate and latency hold for a set time it goes to everyone; if not, it rolls back automatically. Anything risky is behind a feature flag, so deploying the code and releasing the feature are separate steps.
Covers checks, review, merge queue, one artifact, progressive rollout and flags, in order.
Interviewer
CI takes 35 minutes. What do you do?
You
First I look at the timing for each job and work on the longest bar, because nothing else changes the total. Usually that means caching dependencies, running independent jobs in parallel and sharding the browser tests. In a monorepo I would only test what the change affects. I would watch the median CI time and our lead time for changes to check it worked, and I would expect pull requests to get smaller once waiting stops hurting.
Measures first, fixes the critical path, and names how to verify the result.

Checkpoint#

Checkpoint

1. What separates continuous delivery from continuous deployment?

2. Two pull requests each pass CI alone, but break main when both merge. What prevents this?

3. Which pair of DORA metrics measures stability?

Worth memorising
  • CI keeps it working, continuous delivery keeps it releasable, continuous deployment releases it.
  • Under 10 minutes, people wait for CI. Over 20, they batch changes, which brings back big risky merges.
  • Cache, parallelise, shard. Then fix whichever bar is now the longest.
  • Merge queues keep main green. Pin third party actions to a commit SHA.
  • DORA: deployment frequency, lead time, change failure rate, recovery time.
Say this in 60 seconds

Continuous integration means everyone merges to main at least daily and every push is built and tested automatically, with a red build fixed first. Continuous delivery adds that every passing change is releasable at the press of a button; continuous deployment removes the button, so passing changes ship automatically, usually as a canary and behind feature flags. The pipeline's key number is how long the author waits: past about twenty minutes people batch their changes, so I cache dependencies, run jobs in parallel, shard browser tests and fix the longest job first. A merge queue tests each change on top of the ones ahead of it so main stays green, and I pin third party actions to commit SHAs and use OIDC instead of stored cloud keys. I measure delivery with the four DORA metrics: deployment frequency, lead time, change failure rate and recovery time.