All posts

code-freeze

How to Set Up Recurring Code Freeze Windows

A recurring code freeze runs on a schedule automatically. Here is how to configure Friday windows, quarterly freezes, and biweekly release patterns in RRULE.

Cody Flynn··7 min read

A recurring code freeze is a freeze window that activates automatically on a schedule, without anyone starting it manually each time. You define the pattern once: "every Friday at 3pm for 70 hours" or "first day of each month at midnight for 24 hours," and the freeze appears and disappears on its own. No Slack reminder, no manual branch protection toggle, no post-mortem about the time someone forgot.

Most teams that run regular freezes start with one-off windows and graduate to schedules once the pattern becomes predictable. If you are locking main every Friday by hand, a recurring schedule pays for itself on its first week. If you are at the start of Q3 and thinking about an end-of-quarter freeze in September, this is the right time to configure it.

What patterns do recurring freeze windows cover?

The four schedules that come up most often:

Weekly deploy windows. Many teams freeze deployments every Friday afternoon through Monday morning, protecting weekends when on-call coverage is thin. The freeze prevents end-of-week changes from sitting in production unattended for two days with nobody watching.

End-of-quarter freezes. Finance teams, sales orgs, and companies with quarterly reporting tend to freeze production for the last week or two of the quarter. The risk calculation shifts: a botched deploy during a month that makes up 30% of annual revenue is a different order of magnitude from a typical Tuesday outage.

Release cycle freezes. Teams that ship on a fixed cadence (biweekly, monthly) often freeze the day before and the day of a release to give QA and release managers a stable target.

Security patch windows. Some security teams freeze non-critical changes when a high-severity CVE drops, giving the fix time to land and propagate before anything else changes. These are usually one-offs rather than recurring schedules, but the same mechanism applies.

See what is a code freeze for a broader look at when a freeze is the right tool.

How does RRULE scheduling work?

Recurring freeze schedules use the iCalendar recurrence rule format, RFC 5545, called RRULE. It is the same format Google Calendar and most calendar systems use for repeating events. Once you understand the structure, it reads clearly.

The core components:

  • FREQ — the base frequency: DAILY, WEEKLY, MONTHLY, YEARLY
  • BYDAY — which days (for FREQ=WEEKLY): MO, TU, WE, TH, FR, SA, SU
  • BYHOUR / BYMINUTE — what time, in the schedule's configured timezone
  • BYMONTHDAY — specific day of the month (for FREQ=MONTHLY)
  • INTERVAL — repeat every N periods (INTERVAL=2 with FREQ=WEEKLY means every two weeks)
  • COUNT or UNTIL — end conditions; omit both for an indefinite schedule

Practical examples:

# Every Friday at 3:00 PM
FREQ=WEEKLY;BYDAY=FR;BYHOUR=15;BYMINUTE=0;BYSECOND=0

# Every two weeks on Friday at 4:00 PM (biweekly release freeze)
FREQ=WEEKLY;INTERVAL=2;BYDAY=FR;BYHOUR=16;BYMINUTE=0;BYSECOND=0

# First day of every month at midnight
FREQ=MONTHLY;BYMONTHDAY=1;BYHOUR=0;BYMINUTE=0;BYSECOND=0

# Every weekday at 6:00 PM (nightly deploy cutoff)
FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;BYHOUR=18;BYMINUTE=0;BYSECOND=0

The duration is separate from the RRULE. You set how long each occurrence lasts in minutes. A Friday freeze starting at 3pm that runs through Monday at 9am is 4,200 minutes (70 hours). The RRULE defines when it starts; the duration defines when it ends.

Common schedule configurations

PatternRRULEDuration
Friday-to-Monday weekend freezeFREQ=WEEKLY;BYDAY=FR;BYHOUR=15;BYMINUTE=0;BYSECOND=04,200 min (70 hr)
Biweekly release freeze (Friday)FREQ=WEEKLY;INTERVAL=2;BYDAY=FR;BYHOUR=16;BYMINUTE=0;BYSECOND=02,880 min (48 hr)
Start-of-month stability windowFREQ=MONTHLY;BYMONTHDAY=1;BYHOUR=0;BYMINUTE=0;BYSECOND=01,440 min (24 hr)
Nightly deploy cutoff (weekdays)FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;BYHOUR=18;BYMINUTE=0;BYSECOND=0900 min (15 hr)

How should you scope a recurring freeze?

A recurring schedule generates freeze windows, and each window inherits the scope defined in the schedule's default rules. That scope determines which repos, branches, and environments the freeze covers.

For a Friday weekend freeze at most companies, the right scope is the production environment of the services customers interact with directly, not everything in the org. Freezing internal tools or staging environments on Friday afternoons adds friction without reducing incident risk.

Useful scope patterns:

  • repo_pattern: myorg/*-api — catch all API services without naming each one
  • environment_pattern: prod* — cover production and production-adjacent environments (prod, production, prod-us-east-1)
  • branch_pattern: main — protect the trunk where releases cut from

You can run multiple schedules with different scopes simultaneously. A security team might freeze the payment service every two weeks on Thursday; the platform team might freeze all services at end of quarter. Each schedule is independent, and their windows stack without interfering.

What about timezone differences?

A Friday freeze starting at 3pm ET activates at 8pm for your London team and at midnight Saturday for Singapore. This is usually fine: the intent is to protect the weekend, and all three teams experience the same freeze window. The freeze runs on a single clock, not per-timezone.

Where it gets complicated: a "last day of quarter" freeze that starts at midnight UTC activates on December 30th for teams operating in UTC-1 or earlier. For globally distributed teams, pick a timezone that reflects where your business risk is highest and document it explicitly. "The freeze starts at 3pm New York time" is unambiguous in a way that "end of business Friday" is not.

How do you handle exceptions?

No schedule survives contact with the real world perfectly. The question is how to handle exceptions without defeating the purpose.

Legitimate reasons to override a specific occurrence:

  • A critical security patch has a disclosure window that lands during the freeze
  • A payment processor announces a maintenance window that requires a coordinated deploy
  • A regulatory change has a hard effective date that falls inside a planned freeze

For these, the right path is an emergency override with dual approval, not disabling the schedule. The freeze stays active for everything else while the specific change clears. Disabling the schedule for that occurrence, or pausing the entire freeze, opens the window to every other pending change, which is usually not the intent. See how to handle emergency deploys during a freeze for the full override workflow.

If you find yourself overriding a recurring freeze more than once a quarter, that is a signal the schedule is too aggressive for your team's actual release pattern. Adjust the scope or duration rather than layering on exceptions indefinitely.

How NoShip handles recurring schedules

NoShip's recurring schedule feature is built directly on RRULE. You define the pattern, duration, timezone, and default rules once, then enable the schedule. NoShip creates each freeze window automatically at the scheduled time, applies the same rule set, and closes it when the duration expires.

From the schedule detail page you can preview upcoming occurrences: you can see that the Q3-end freeze will activate on September 29th and close October 1st, and catch any conflicts with a planned release before they happen.

The schedule generates standard freeze windows, which means emergency overrides, audit logging, Slack notifications, and Google Calendar sync all work exactly as they do for manually created freezes. The schedule is just the creation mechanism; the rest of the system behaves the same way.

Full configuration details are in the concepts guide. If you are also blocking deployments in addition to merges, the how to freeze deployments in GitHub guide covers the deployment protection rule side.

What is worth automating vs. keeping manual?

Recurring schedules make sense when the timing is predictable, the scope applies consistently, and the team is large enough that a Slack reminder is not a reliable control.

One-off freezes still make sense for:

  • Unplanned events (a major dependency has an outage, a release slips two weeks)
  • Narrow scope that does not generalize (freeze one repo for one specific deploy window)
  • Testing a new freeze policy before committing to a recurring pattern

There is no overhead to keeping both. A recurring schedule handles the predictable pattern; one-off freezes handle everything else. The two coexist and their windows stack correctly without configuration.

The right time to set up a recurring schedule is before the next occurrence, not during it. If you are starting Q3 now, the end-of-September freeze is three months away and not yet urgent. Set it up anyway, preview the occurrences, confirm the timing, and forget about it. That is the whole point.

Keep reading