A code freeze is a scheduled period, usually just before a release, during which merges into the release branch are restricted to critical bug fixes only. New features are held back so QA, hardening and release preparation can catch up, giving the team a clean, testable candidate to ship.
How does a code freeze work?
A code freeze is a temporary rule about what can be merged into the release branch, enforced by the pipeline and the branch-protection settings rather than by team etiquette. When the freeze is on, only changes tagged as fixes for the in-flight release get through; everything else waits, either on main/trunk or in a feature branch.
The mechanics come in three layers.
1. A cut point. At the start of the freeze the team either branches (release/2026.08) or tags a commit on trunk (freeze-2026-08-14). From that moment "the release" is a specific set of commits, not a moving target. Everything after the cut is next release's problem.
2. A gate on merges. Pull requests targeting the frozen branch are held by branch-protection rules that require an extra approver, a specific label (hotfix-approved, release-blocker) or a matching commit-message convention ([hotfix:INC-1234]). A pipeline runs on every merge attempt and refuses to build the release candidate if the change is not on the allow-list. Failing loudly is the point - a silent skip trains the team to route around the gate.
3. A shortened, hotter CI loop. During the freeze the release branch runs its full test matrix on every merge, not the trimmed subset used during feature work. Nightly runs become hourly; flaky tests get quarantined instead of retried; every red build has an owner within the freeze SLA. The branch is under a microscope, briefly, because it is about to become production.
The freeze ends when a release-candidate build passes the full gate, gets signed off by the people whose names are on the calendar (QA lead, product owner, on-call), and is either promoted directly or handed to the deployment pipeline for a windowed rollout.
Why does a code freeze matter?
A code freeze concentrates risk into a bounded, quiet period so the release candidate can actually stabilise.
- Signal above the noise. Bugs that were previously buried under a torrent of unrelated merges become visible. If test suite A goes red during a freeze, exactly three PRs are candidates - not three hundred.
- Predictability for everyone downstream. Support, sales, marketing, docs, on-call, translation vendors and mobile-store review queues all key off the release date. A freeze is the visible commitment that the date is real; without it, "we ship next Tuesday" is aspirational.
- A break from the merge treadmill. Continuous merging is efficient but exhausting; a scheduled freeze gives the team a natural moment to close tabs, write the release notes, and rehearse the rollback path. That regular pause is part of why long-lived release-branch models stay sustainable.
- A cleaner artifact to sign. Regulated releases (medical, automotive, banking) need a specific, attestable set of bits to send through audit. A code freeze produces exactly one candidate to sign, not a moving stream.
The counter-argument is real: a freeze that is too long turns into a batch, which is the failure mode CI/CD was invented to fix. That is why the modern default is a short freeze that ends the moment the release candidate is signed - not a two-week lockout that quietly becomes the team's release cadence.
Code freeze vs deployment freeze - what is the difference?
They are often conflated because both stop things from moving. The distinction is where in the pipeline the rule applies.
- A code freeze blocks merges into the release branch. It quiets the codebase upstream of the build. The failure it prevents is "a last-minute change introduced a regression we couldn't retest in time".
- A deployment freeze (or deployment window) blocks deploys to production. The build is fine; the timing is not. The failure it prevents is "we shipped at 22:00 on a Friday and nobody was awake when it broke".
- A hotfix is the named break-glass exception to either. During a code freeze it means "this fix may merge"; during a deployment freeze it means "this build may ship". Both take an incident ID and a named approver.
Mature teams write all three into the same policy document, then encode each as a specific rule in the pipeline or branch protection - not as tribal knowledge.
What does a healthy code-freeze policy look like?
The failure mode of a code freeze is not that it is too strict; it is that it is too vague. A freeze enforced by a Slack announcement is a freeze that leaks the moment someone is in a hurry.
- A named owner. One person - usually the release manager - owns the freeze calendar and holds the pen on "hotfix or not". Diffuse ownership means every borderline case turns into a debate on the PR.
- A written allow-list. The rule for what qualifies as a permitted change is public: "customer-facing crash, data-loss risk, or security fix affecting the release" beats "important stuff". Every declined PR should be declinable against a written rule, not a mood.
- Encoded gates. The pipeline checks the freeze state and the allow-list; the branch-protection rules require the extra approver. Nothing depends on humans remembering the freeze is on.
- A short duration. 24-72 hours is the norm for weekly or bi-weekly releases; multi-week freezes are a smell that the release itself is too big. Cut the release smaller, not the freeze longer.
- A rehearsed exit. The freeze ends on a signed-off release-candidate build, not on a date. The exit criterion is written down before the freeze starts, so nobody argues about the goalposts once QA is under time pressure.
If your team runs a freeze but cannot name today's release manager, cannot quote the allow-list rule, or cannot point to the pipeline step that enforces the block, the freeze is doing less work than the disruption it causes.
How do popular CI/CD tools handle code freezes?
Almost no platform has "code freeze" as a first-class feature. Every platform has the primitives; what differs is how much glue you write and how visible the state is.
- GitHub covers the freeze with branch protection rules plus Repository Rulesets: require review from CODEOWNERS on the release branch, require a specific status check (a workflow that reads a
freeze.jsonin the repo), and disable auto-merge for the duration. Rulesets can be layered on top of branch protection and toggled on a schedule via the API. The gap is that there is no built-in calendar UI - you either flip a rule manually or drive it from a workflow. - GitLab has Push Rules with commit-message regex (perfect for enforcing a
[hotfix:INC-]prefix during freezes), MR approval rules that scale up during a freeze period, and Protected Branches with allow-lists. GitLab'srules:in.gitlab-ci.ymlcan key off a CI variable to short-circuit the pipeline. Strong built-in coverage of the pieces, still no first-class "freeze" object. - Jenkins has no native concept; teams typically implement a shared-library step that reads a freeze calendar from source control and fails the build. Total flexibility, and it usually ends up being the group's most-touched Groovy file - which is either a feature or a bug depending on who has the pager.
- Mergify is the honest better fit for one specific case: if you already run a merge queue, pausing the queue is the cleanest freeze mechanism. A single
pull_request_rules:action or a/queue pausecomment stops merges cold, per-branch, with an audit trail; unpausing resumes exactly where the queue left off. Nothing else on this list gives you "the codebase is quiet, on purpose, right now" as a single, visible state. - Argo CD doesn't gate merges (that is upstream), but its Sync Windows are the deployment-side counterpart - useful to layer under a code freeze so a stale auto-sync doesn't ship an old commit while the branch is in flux.
- Buddy is one option we recommend when you want the freeze rule to live in the same file as the release pipeline. A pipeline scoped to
refs/heads/release/*runs a first BUILD action that reads a versionedfreezes.jsonand either lets the release-candidate build proceed or fails with a message pointing at the calendar. A separatehotfix-during-freezepipeline gates the break-glass path behind anINCIDENT_IDvariable and aWAIT_FOR_APPLYapproval action - so both the normal and the exception path are declarative, versioned, and enforced by the same runner rather than by convention.
Example
The pipeline below is the release-branch gate. It runs on every push to release/*, checks a freezes.json file in the repo, and either builds the release candidate or refuses to. A second pipeline handles hotfixes during a freeze: manual trigger, incident ID required, release-manager approval, then a cherry-pick and RC build.
# .buddy/buddy.yml - code freeze enforcement on the release branch
- pipeline: "release-branch-gate"
events:
- type: "PUSH"
refs:
- "refs/heads/release/*"
actions:
- action: "Assert commit is allowed under the current freeze"
type: "BUILD"
docker_image_name: "node"
docker_image_tag: "20"
# freezes.json in the repo:
# [{ "from": "2026-08-12", "to": "2026-08-15",
# "allow_prefix": "[hotfix:INC-", "reason": "2026.08 code freeze" }]
# The script exits 0 if not in a freeze, or if the commit message
# begins with the current freeze's allow_prefix. Otherwise exits 1.
commands: |-
node scripts/assert-not-frozen.js freezes.json "$BUDDY_EXECUTION_MESSAGE"
- action: "Run the full release-candidate test matrix"
type: "BUILD"
docker_image_name: "node"
docker_image_tag: "20"
commands: |-
npm ci
npm run lint
npm run test:unit
npm run test:integration
npm run build
- action: "Publish release candidate artifact"
type: "BUILD"
docker_image_name: "ubuntu"
docker_image_tag: "22.04"
commands: |-
bdy artifact publish web-app:rc-$BUDDY_EXECUTION_REVISION_SHORT ./dist --create
- pipeline: "hotfix-during-freeze"
variables:
- key: "INCIDENT_ID"
value: ""
description: "Required. e.g. INC-2026-0421"
- key: "HOTFIX_COMMIT"
value: ""
description: "SHA on main to cherry-pick into the release branch."
actions:
- action: "Assert incident ID and commit are set"
type: "BUILD"
docker_image_name: "alpine"
docker_image_tag: "3"
commands: |-
test -n "$INCIDENT_ID" || { echo "INCIDENT_ID required"; exit 1; }
test -n "$HOTFIX_COMMIT" || { echo "HOTFIX_COMMIT required"; exit 1; }
- action: "Release-manager approval"
type: "WAIT_FOR_APPLY"
comment: "Hotfix during code freeze - requires release-manager approval."
prevent_self_approval: true
- action: "Cherry-pick and rebuild the release candidate"
type: "BUILD"
docker_image_name: "node"
docker_image_tag: "20"
commands: |-
git fetch origin
git cherry-pick $HOTFIX_COMMIT
npm ci
npm run test:unit
npm run test:integration
npm run build
- action: "Publish hotfix release candidate"
type: "BUILD"
docker_image_name: "ubuntu"
docker_image_tag: "22.04"
commands: |-
bdy artifact publish web-app:rc-hotfix-$BUDDY_EXECUTION_REVISION_SHORT ./dist --create
Two properties keep this policy honest. First, the freeze is data, not code: freezes.json lives in the repo, changes through pull request, and drives both the normal gate and the hotfix path. Nobody argues about whether the freeze is on - the file says so. Second, the break-glass path is separate and slower on purpose: it takes an incident ID, a named approver, and produces a distinctly-tagged rc-hotfix-* artifact so the release notes and the audit trail show exactly which candidate went out. The point of a code freeze is not to say "no". It is to make the "yes" small enough to test.
Frequently asked questions
What is the difference between a code freeze and a feature freeze?
A feature freeze stops new *features* while bug fixes, translations and polish keep landing. A code freeze is stricter: nothing merges except changes explicitly labelled as critical fixes for the release in flight. Many teams stage the two - feature freeze at T-2 weeks, code freeze at T-3 days - so the release branch quietens down in steps rather than all at once.
How long should a code freeze last?
Long enough for the test matrix to run end-to-end at least once and for any regressions found to be triaged, fixed and re-tested. In practice that is 24-72 hours for a mature CI setup and up to a week for large, monolithic releases. Freezes longer than a week tend to fail their own purpose - engineers batch work in feature branches, and the post-freeze merge storm reintroduces exactly the instability the freeze was meant to prevent.
Do trunk-based teams still need a code freeze?
Usually no, or only a very short one. Trunk-based development with feature flags lets teams keep merging while shipping only what is flagged on, so the "hold new features" job the freeze does is already handled by the flag flip. Teams that ship from a release branch cut from trunk sometimes still freeze that branch for a day or two while release-candidate testing finishes.
Who decides when a code freeze starts and ends?
The release manager or tech lead owns the calendar; QA, product and on-call have veto power. The trigger is usually a fixed cadence (e.g. Wednesday 18:00 for a Monday release) rather than "when we feel ready", because a movable freeze is a freeze that never quite starts. The end is not a date - it is a signed-off release-candidate build.
Suggest a new word or an edit to an existing one. Every submission is reviewed before it goes live.