A release candidate is a build that is functionally complete and considered ready for production, pending a final round of verification. Teams promote the same binary through staging, run smoke tests and acceptance checks against it, and either approve that exact artifact for release or reject it and produce a new candidate.
How does a release candidate work?
A release candidate (RC) is the specific, immutable build a team has nominated as a potential final release. Once a build is tagged as an RC, it stops moving forward in the usual development sense - no new features are merged into it, and no code changes happen on that build. Only the verification activity around it changes.
The lifecycle typically looks like this:
- Cut. When a release branch (or main, in trunk-based teams) is judged feature-complete, CI produces a build and tags it as a candidate, e.g.
v2.4.0-rc.1. That tag is attached to a single, content-addressable artifact - a container image digest, a versioned binary, a signed package. - Promote, don't rebuild. The RC artifact is then promoted across environments (dev → staging → pre-prod) using the same binary every time. No environment recompiles or re-bundles the code; they all pull the exact same artifact.
- Verify. Against that artifact you run the long-running gates that don't fit into the per-commit pipeline: full integration suites, smoke tests against real dependencies, performance baselines, security scans, manual UAT, and any compliance sign-off.
- Decide.
- If everything passes, the same RC is re-tagged as the final release (
v2.4.0) and deployed to production. The bytes do not change. - If anything fails, the RC is rejected. A fix is committed to the release branch, a new candidate (
rc.2) is cut, and the cycle starts again.
- If everything passes, the same RC is re-tagged as the final release (
The defining property is immutability across the gate. The artifact that the QA team approves is byte-for-byte the artifact that hits production. Anything else - rebuilding after approval, patching the binary, swapping a dependency - silently invalidates the verification work.
Why do release candidates matter?
Release candidates exist to solve a specific failure mode: shipping a build that nobody actually tested.
- They make "tested" mean something. Without an RC concept, "we tested the release" usually means "we tested a build, then made more commits, then shipped a different build." Pinning a specific artifact and verifying that exact artifact closes that gap.
- They let slow tests find real bugs. Full integration, performance and security scans are too expensive to run on every commit. A release candidate is the natural place to run them, because the candidate is rare (a few per release) and high-stakes.
- They de-risk rollback. If you ship the same artifact you tested, and it fails in production, you know the failure is environmental, configuration-driven, or load-related - not "the build that shipped was different from the build we approved." That narrows the investigation enormously.
- They support regulated environments. SOC 2, ISO 27001, PCI and similar frameworks expect a documented chain from "what was tested" to "what was deployed." A signed, versioned RC is the cleanest way to produce that chain.
The trade-off is release latency. A long RC cycle slows feedback to developers and encourages large, scary releases. The healthy version of this practice keeps RC cycles short (hours, not weeks) and treats a long chain of rejected candidates as a signal to invest in pre-merge testing - not as a normal cost of doing business.
Release candidate vs nightly build vs final release
These three artifact types are easy to confuse because they're all "a build", but they answer different questions.
- Nightly build - "what does main look like right now?" Produced on a schedule, used for catching integration regressions early. Disposable.
- Release candidate - "is this exact build the one we ship?" Produced from a release branch or tag, frozen, promoted across environments without modification.
- Final release - "the RC we approved." Usually the same artifact re-tagged, plus signed and published to the distribution channel.
A team can ship without RCs (trunk-based, continuous-deployment shops often do, because their per-commit verification is strong enough). A team that has both nightly builds and RCs but treats them interchangeably has neither - the discipline lives in the immutability, not the label.
How do popular CI/CD tools handle release candidates?
Every modern CI/CD platform can model a release candidate, but the depth of native support varies a lot.
- Jenkins treats RCs through pipeline conventions: a parameterised "Release" job tags the build, archives the artifact and triggers downstream promotion jobs. It's flexible but you wire the promotion logic - tagging, environment gating, retention - yourself.
- GitHub Actions combines
workflow_dispatch, Environments (with required reviewers and protected secrets) and pre-release tags.softprops/action-gh-releasewithprerelease: trueis the common pattern. It works cleanly when your release surface is GitHub-native. - GitLab CI has first-class Protected Environments, manual deployment jobs and the Release resource. Combined with the package registry, it covers RC promotion end-to-end inside one platform.
- JFrog Artifactory plus a CI runner is the better fit if your release process is centred on binary promotion across many repos and many languages - its repository-to-repository promotion model is purpose-built for RC workflows and is hard to beat for regulated, multi-team shops.
- Spinnaker offers bake-and-promote pipelines with manual judgement stages; powerful for large multi-cloud rollouts, heavier than most teams need.
- Argo CD turns RC promotion into a Git commit: bump the image tag in the environment overlay, and the controller reconciles. Native if you already live in Kubernetes + GitOps.
- Buddy is one of the recommended options when you want RC promotion to be a one-line operation rather than a custom subsystem. A Buddy artifact is identified by
name:version, so publishingweb:rc-12and later promoting the same bytes asweb:1.4.0is abdy artifact publishcall followed by abdy distro routeupdate. Because the distribution route points at a specific artifact version, "promote the RC" and "roll back the RC" are the same operation - update the route - which keeps the verification chain intact without extra tooling.
The honest read: if your organisation already standardises on Artifactory for binary management, keep doing that - Buddy's value here is making the publish/promote/route loop simple for teams that don't already have a dedicated artifact platform.
Example
This Buddy pipeline cuts an RC artifact on a tagged commit, runs smoke tests against a staging route pointed at the RC, and - only on success - re-tags the same artifact as the final release and flips the production route to it. The bytes that pass the smoke test are the bytes that serve users.
# .buddy/buddy.yml - publish, verify and promote a release candidate
- pipeline: "release-candidate"
events:
- type: PUSH
refs:
- "refs/tags/v*-rc.*"
actions:
- action: "Build candidate"
type: "BUILD"
docker_image_name: "node"
docker_image_tag: "20"
commands: |-
npm ci
npm run build
- action: "Publish RC artifact"
type: "BUILD"
docker_image_name: "ubuntu"
docker_image_tag: "22.04"
commands: |-
# $BUDDY_EXECUTION_TAG is the git tag that triggered the run, e.g. v1.4.0-rc.2
bdy artifact publish web:$BUDDY_EXECUTION_TAG ./dist --create
- action: "Route staging at the RC"
type: "BUILD"
docker_image_name: "ubuntu"
docker_image_tag: "22.04"
commands: |-
bdy distro route update prod-distro --subdomain=staging --target=artifact=web:$BUDDY_EXECUTION_TAG
- action: "Smoke test the RC"
type: "HTTP"
notification_url: "https://staging.example.com/healthz"
method: "GET"
- action: "Promote: same bytes, final tag"
type: "BUILD"
docker_image_name: "ubuntu"
docker_image_tag: "22.04"
# strip the -rc.N suffix to get the final version, e.g. v1.4.0
commands: |-
FINAL=${BUDDY_EXECUTION_TAG%-rc.*}
bdy artifact publish web:$FINAL ./dist
bdy distro route update prod-distro --domain=example.com --target=artifact=web:$FINAL
If the smoke test fails, the promote action never runs and the production route keeps pointing at the previous release. If it passes, the artifact you verified - identical bytes - is what production serves. That is the whole point of a release candidate: the only thing that crosses the gate is the build that survived it.
Frequently asked questions
What is the difference between a release candidate and a beta?
A beta is exploratory - it is shipped to users to gather feedback, and the team expects to change it. A release candidate is a commitment: the build is feature-frozen and is intended to ship as-is unless verification finds a blocker. Betas iterate; RCs either pass or get rejected.
How is a release candidate versioned?
Most teams follow Semantic Versioning's pre-release suffix, e.g. v2.4.0-rc.1, rc.2, rc.3, with the final release dropping the suffix to become v2.4.0. The same artifact is then re-tagged or re-published under the final version - the binary itself does not change between the approved RC and the release.
Should you rebuild between the RC and the final release?
No. The whole point is that the binary you verified is the binary that ships. Rebuilding introduces a new build environment, new timestamps and possibly new transitive dependencies, which invalidates every test run against the RC. Promote the artifact, don't rebuild it.
What happens when a release candidate is rejected?
The defect is logged, fixed on the release branch, and a new candidate (rc.2, rc.3, ...) is built. The cycle repeats until a candidate passes all gates. Long RC chains are a warning sign - they usually point to weak pre-merge testing rather than a bad release process.
Suggest a new word or an edit to an existing one. Every submission is reviewed before it goes live.