We deploy to production throughout the day, every working day. Once a task is finished, the tests go green and the pull request has passed code review, it goes to production. There’s no release train, no release window, no release manager and no Wednesday release day.
If you're used to release cycles, that probably sounds reckless. But it isn't. And the reason it works has very little to do with tooling. This post is about how we got here and what makes this way of working safe.
Release day used to be a real day
More than ten years ago, we ran different release cycles for different partners. Some of them were on a two- to three-month cadence. That meant three months of development, then a testing cycle, then a release.
The releases always had bugs. That was not surprising: three months of accumulated change going out at once. A release day wasn't exactly one day. It was more like a season: the release itself, followed by a few weeks of fixing what it had broken. Everyone knew it was coming and dreaded it. And because every partner was on their own schedule, we were never between releases. We were always in the middle of somebody else's release.
We decided to consolidate everything into a single weekly release cycle, with everyone working on the same schedule and releases going out once a week. On Wednesdays.
This was a genuine improvement, but it still did not fix the problem, because developers can write a lot of code in a week. Even a week's worth of changes is still a big pile, and it still went out in one motion.
Wednesday mornings were tense. By Wednesday afternoon, we were fixing bugs and we often had to continue into Thursday. We had made release days less catastrophic by making them more frequent, which arguably just meant being stressed more often.
What we eventually realised was that the stress didn't scale with how often we released. It scaled with how much change went out in one batch.
Making deploys boring
Deploying shouldn’t be something you have to schedule, let alone something you brace yourself for months in advance. So we took the idea to its logical conclusion and made deploying to production part of the daily routine, just like opening a pull request. It became a normal part of finishing a task.
From then on, each release was a single task handled through a pull request. The code was reviewed, run through the automated test suite and then deployed by the person who wrote it.

The last two steps are the ones people find surprising. Deploying isn't a button in a dashboard or a job someone triggers. It’s done through a pull request. Merging the first deploys the build to stage and merging the second deploys it to production. Because production changes go through the same pull-request workflow we already use for everything else, they’re reviewable and logged, and nobody has to learn a separate deployment tool.
Stage is a real environment, not just a step before production. It's where database migrations actually run, where integration between services is exercised, and where we verify the deployment itself works. It's the last place where problems are still relatively cheap to fix.
The direct consequence of task-sized deploys is that a lot of what reaches production isn't a finished feature. It might be a part of something larger, a database migration whose consumer isn't written yet or a new API endpoint no service is calling. That's fine, because deploying is not the same as releasing. Code can sit in production for weeks before any user reaches it.
The following is what makes that separation possible.
Backwards compatibility is a rule, not a decision
Everything else depends on this rule, and it's a policy, not a technique.
Our APIs do not introduce backwards-incompatible changes. We don’t debate that every time a change comes up. Additive changes are generally straightforward, but removing something that already exists, or changing what it means, takes deliberate planning and coordination.
The same rule applies to database migrations, which is why we’ve never had to roll one back. We don't write a migration that the currently running application can't tolerate. If a field needs to go away, we remove it in two steps, across two pull requests, in this order:

Doing it in two steps means an extra pull request and an extra deployment. But once we made our deployments boring, the cost of that extra step became tiny. That’s what makes the whole approach work: because deployments are cheap, we can break changes into smaller, safer steps instead of trying to do everything at once.
Large tables get one more piece of machinery. Migrations that would lock a large table for a meaningful amount of time run through Percona's online schema change tool, so the table remains readable and writable while the change is applied.
The layers before production
None of the following practices are remarkable on their own. What matters is how they work together to make small deploys safe enough to become routine.
Tests run twice. Unit and integration tests run on every push to a feature branch, so reviewers aren’t spending time on code already known to be broken. The tests run again after the merge to master, this time alongside the end-to-end suite. E2E is expensive enough that running it on every push would slow developers down for little benefit, so it guards the branch that actually ships.
By the time a deploy pull request exists, the code has already been through multiple rounds of testing. The final run confirms rather than discovers. This is the deliberately boring part of the pipeline. It only works if it stays fast and predictable. If the suite is slow or flaky, deploys stop being cheap, and the whole model slides back towards batching changes simply to avoid the wait.
Feature flags keep unfinished work invisible. They are plain branching in application code, not infrastructure, and they can send a percentage of traffic down the new path rather than switching it on for everyone at once. That is what lets an unfinished feature sit safely in production: the code is deployed, but the path stays closed.
Canary deployments narrow the blast radius of a bad build by rolling a new version out to a slice of traffic before expanding it. We treat canary deployments as an option rather than a requirement. They add time to a deploy, and for plenty of services the extra safety isn’t worth the wait.
If a bug reaches users, we can roll back quickly. Reverting the deploy commit puts the previous version back. If the broken code is behind a feature flag and no users are reaching it, we fix it and ship the fix forward instead.
Fast feedback
The practices mentioned above are all about prevention: stopping a bad change from reaching production or limiting how much of production it reaches. Fast feedback is different. It assumes something got through anyway, which is why it’s the one we’d argue most strongly for.
Deploying continuously is only reasonable if problems show up within minutes, not at the end of a testing cycle. That means doing the painstaking work of adding enough logs and metrics to every service to understand what is actually happening in production. High traffic helps — a broken path tends to reveal itself quickly — but only if the right metrics and alerts are in place. When error rates rise, alerts are sent to a dedicated channel that’s actively monitored. Support staff follow the channel continuously, and the developer who just deployed usually keeps an eye on it too. With fast feedback, you know almost immediately whether a deploy worked.
Without this, everything becomes a gamble.
We still schedule things, occasionally
None of this means we never plan deployments. Some changes are genuinely risky, for example a very large database migration or a change with an unusual blast radius, and those get a release date. We pick a time, tell the partners it's happening, and make the change deliberately with the right people monitoring it closely.
The difference from the old approach is what we consider normal. Scheduling used to be how everything shipped, with the occasional hotfixes squeezed in between releases. Now continuous delivery is the default method, and scheduling is the exception, reserved for the handful of changes that genuinely warrant it. Those changes get more attention precisely because they're unusual. A scheduled release stands out on an otherwise empty calendar.
Treating ‘always deploy immediately’ as an absolute rule would be just as rigid. The point was never how often we deployed. The problem was that bundling lots of changes together made failures harder to understand and harder to fix.
The part that actually removed the stress
The tools above made continuous deployment possible, but they weren’t what made it less stressful. What really changed things was knowing what had just been deployed when something went wrong.
With a three-month release cycle, whenever something went wrong in production, the first question was always: ‘What changed?’ The honest answer was ‘three months worth of changes’. Figuring out which one caused the problem became an investigation, and we had to do it while the system was already broken.
This was never about blame. It was about finding problems quickly so we could fix them quickly.
With task-sized deploys, that question is much easier to answer. If errors start appearing, the recent deploys form a short, ordered list, with a developer attached to each one. The person who deployed the change is responsible for checking what it does in production because they have the most context and are usually the quickest person to work out what went wrong.
That’s what really took the anxiety out of deploying.
What it takes
Making this work takes more than good deployment tooling. It depends on keeping changes always backwards compatible, maintaining a test suite that is fast and reliable, and keeping pull requests small enough that reviewing and deploying them is routine. Feature flags need the same discipline: they should be temporary. Once a flag outlives the feature it was created for, it becomes another branch in the code that nobody quite remembers why it exists.
If you deploy once a quarter, most of this is unnecessary overhead. It only starts to pay off when deploying becomes a daily routine, but deploying only becomes routine once the tooling and practices are in place. That's the awkward part: you have to build for the cadence you want before you can actually work at that cadence.
For us, it was worth it. Release day used to be the most stressful recurring event in our engineering calendar. We deleted it by turning releases into something that happens all day, every day.
By Kaspar Soer, Lead Architect at Car Rental Gateway.


