The Meeting That Should Have Been a Deploy
There is a specific kind of slowness that settles into engineering teams gradually enough that nobody notices it happening. It’s not the slowness of technical debt, where old decisions constrain new ones. It’s not the slowness of scale, where systems that worked at one size stop working at ten times the size. It’s the slowness of process — the accumulation of steps between deciding to do something and having it done.
I’ve watched it happen at companies of every size. The team that used to ship three things a week now ships three things a month. The codebase is not significantly larger. The team is not significantly smaller. The engineers are not less capable. The distance from idea to production has just quietly doubled, then doubled again, in increments small enough that no individual increment felt significant.
The way this slowness kills teams is not through drama. Nobody calls a meeting to announce that the team has become too slow to compete. It happens through attrition: the engineers who care most about shipping leave first, because engineers who care about shipping can find somewhere else to do it. The ones who remain are either comfortable with the pace or too embedded to leave. The product stagnates. By the time the problem is visible from the outside, it has been visible from the inside for a long time.
How the path gets longer
The path from decision to production has distinct stages, and process accumulates at each one.
The decision stage. Early in a company’s life, decisions are made by the people doing the work. A developer sees a problem, forms an opinion, proposes a solution, builds it. The decision loop is short because the decision-maker and the implementer are the same person.
As companies grow, decisions acquire stakeholders. A change to the API needs sign-off from the team that consumes it. A change to the database schema needs review from the DBA. A new feature needs approval from product, from legal, from security. Each of these requirements exists for a reason. The API team genuinely is affected by breaking changes. The schema change genuinely does need review. Legal genuinely does need to know about certain features.
But the process that was designed to catch the important cases gets applied uniformly to every case. The one-line bug fix needs the same review cycle as the architectural change. The text copy update needs the same approval chain as the new data collection feature. The process doesn’t distinguish between the change that genuinely needs ten people in a room and the change that genuinely needed one person with a keyboard.
The review stage. Code review is good. Code review that takes three days for a two-line change is not good — or rather, it’s not code review that’s the problem, it’s the ratio between the time the code sits waiting and the time the reviewer spends reading it.
Most code review queues are not bottlenecked by reviewer capacity. They’re bottlenecked by reviewer attention. A developer with five open review requests processes them in the order of least resistance, which tends to mean smallest first, which means the large important changes sit longest. The small changes that don’t need careful review get reviewed quickly. The large changes that most need careful review get reviewed last, rushed, or broken into smaller pieces not because smaller is better for the change but because smaller is what gets reviewed.
The testing stage. Test suites that take forty-five minutes to run are not primarily a cost in compute time. They’re a cost in developer attention. A developer who pushes a change and then does other work while the tests run has context-switched by the time the tests finish. If the tests fail, they’re returning to a problem they’ve partially forgotten. If the tests pass, they’re approving something they’ve partially moved on from. The slower the feedback loop, the more it costs to act on the feedback.
The deployment stage. Deployments that require a manual step, a specific person’s involvement, a coordination window, or a scheduled release train are deployments that accumulate queue. Small changes wait to be bundled. Bundled changes are riskier than small ones. Riskier deployments require more process to approve. More process means more wait. The cycle compounds.
The individual components of this accumulation are often defensible in isolation. The approval step was added because of an incident. The review requirement was added because of a bad merge. The deployment window was added because of customer impact. Each addition was a reasonable response to a real problem. The aggregate effect is a team that ships at a fraction of its capacity.
What a day actually looks like
I want to be concrete about what process accumulation does to a developer’s day, because the abstract accounting of “lost time” misses the texture of what actually happens.
A developer has a change ready to ship. It’s a small change — a fix to a validation error that’s been affecting a subset of users for two days. They open a pull request. The required reviewers are in meetings until the afternoon. By the time the review happens, it’s 4pm. One reviewer has a question about a related component that’s not actually part of the change. The developer answers the question. The reviewer approves but a second required reviewer hasn’t looked yet. The second reviewer will look tomorrow morning.
Tomorrow morning, the CI pipeline fails on an unrelated flaky test. The developer reruns it. It passes. The second reviewer approves. The change needs to be deployed. Deployments happen in the 2pm window because the team had a production incident three months ago and decided deployments should happen when the whole team is available.
It is now 2pm of the second day. The fix ships. The validation error is resolved. Total elapsed time: forty hours. Total time the change spent being actively worked on: roughly two hours.
The developer has spent two days with this task as an open loop in the back of their mind. They’ve checked the PR status multiple times. They’ve responded to an unrelated question. They’ve rerun a flaky test. They’ve waited for a deployment window. None of this is in anyone’s sprint velocity. None of it shows up as a bottleneck in the retrospective. It’s just the texture of the job, accumulated across every change, every week.
Multiply this by a team of ten developers, each carrying three or four open loops at any time, and you have an organisation that is technically very busy and practically moving very slowly.
The metrics that expose it
Most teams measure the wrong things when they try to understand why they’re slow. They measure story points completed, PRs merged, features shipped. These are output metrics. They tell you how much got done. They don’t tell you how long it waited before getting done.
The metric that exposes process accumulation is cycle time: the elapsed time from when a change was first committed to when it was running in production. Not the time spent actively working on it — the total elapsed time including all the waiting.
Measuring cycle time requires connecting your version control to your deployment pipeline in a way that captures timestamps at each stage. This is not technically difficult. Most teams have never done it because it wasn’t seen as necessary.
# A simple cycle time tracker
# Connects git commit timestamps to deployment events
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Optional
import subprocess
import json
@dataclass
class ChangeMetrics:
commit_sha: str
committed_at: datetime
pr_opened_at: Optional[datetime]
first_review_at: Optional[datetime]
approved_at: Optional[datetime]
merged_at: Optional[datetime]
deployed_at: Optional[datetime]
@property
def review_wait_hours(self) -> Optional[float]:
if self.pr_opened_at and self.first_review_at:
delta = self.first_review_at - self.pr_opened_at
return delta.total_seconds() / 3600
return None
@property
def merge_to_deploy_hours(self) -> Optional[float]:
if self.merged_at and self.deployed_at:
delta = self.deployed_at - self.merged_at
return delta.total_seconds() / 3600
return None
@property
def total_cycle_hours(self) -> Optional[float]:
if self.committed_at and self.deployed_at:
delta = self.deployed_at - self.committed_at
return delta.total_seconds() / 3600
return None
def report_cycle_time(changes: list[ChangeMetrics]) -> dict:
cycle_times = [c.total_cycle_hours for c in changes if c.total_cycle_hours]
review_waits = [c.review_wait_hours for c in changes if c.review_wait_hours]
deploy_lags = [c.merge_to_deploy_hours for c in changes if c.merge_to_deploy_hours]
def percentile(data: list[float], p: int) -> float:
sorted_data = sorted(data)
index = int(len(sorted_data) * p / 100)
return sorted_data[min(index, len(sorted_data) - 1)]
return {
"cycle_time": {
"p50_hours": percentile(cycle_times, 50),
"p95_hours": percentile(cycle_times, 95),
},
"review_wait": {
"p50_hours": percentile(review_waits, 50),
"p95_hours": percentile(review_waits, 95),
},
"deploy_lag": {
"p50_hours": percentile(deploy_lags, 50),
"p95_hours": percentile(deploy_lags, 95),
},
}
When teams measure this for the first time, the numbers are usually surprising. Not because the individual waits are dramatic but because the aggregate is. A p50 cycle time of 72 hours for a team that considers itself to be moving fast is common. Most of those 72 hours are waiting, not working.
Once you can see the waiting, you can ask where it is. Review wait is usually the largest component. Merge-to-deploy lag is usually second. Both are process problems, not engineering problems. They don’t require better code — they require different agreements about how work moves.
The agreements that actually help
When teams try to address slowness, they often reach for more process: better sprint planning, more detailed tickets, clearer acceptance criteria. This is the wrong direction. More process does not reduce cycle time. It adds stages to the path.
The agreements that actually help are the ones that remove steps or shorten waits.
Review time as a commitment, not a best effort. If a PR sits in the review queue for more than four hours during working hours, something has gone wrong. Not as a guideline — as a commitment the team has made to each other. This requires reviewers to treat review requests as interrupts, not as tasks to batch. It requires the team to agree that reviewing someone else’s work is as important as doing your own. Most teams have never explicitly made this agreement, which is why most review queues are measured in days.
Deployment as a routine, not an event. A deployment that requires coordination, a specific window, or a specific person has a queue. A deployment that any developer can trigger at any time, with automated rollback if something goes wrong, has no queue. The technical prerequisites for this — automated testing, feature flags, deployment automation, good observability — are real work. They’re also work that pays for itself within months, because the carrying cost of a scheduled deployment process is enormous.
# The deployment pipeline that enables routine deploys
# Every merge to main deploys automatically
# Feature flags control exposure, not deployment timing
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: npm test
- name: Build
run: npm run build
- name: Deploy to production
run: |
./scripts/deploy.sh \
--image ghcr.io/${{ github.repository }}:${{ github.sha }} \
--environment production
- name: Verify deployment
run: ./scripts/verify-deployment.sh
- name: Rollback on failure
if: failure()
run: ./scripts/rollback.sh
Every merge deploys. The deploy takes eight minutes. If verification fails, it rolls back automatically. No deployment window. No coordination email. No “who’s around to deploy this?”
Change size as a discipline. Small changes have shorter cycle times than large changes for every reason: they’re reviewed faster, they carry less risk, they’re easier to roll back, they’re easier to understand in isolation. The discipline of breaking work into changes that are independently deployable is not always natural — it requires thinking about the order of work differently than most developers are trained to think. But it’s the single highest-leverage change most teams can make to their cycle time, and it costs nothing except the habit change.
Asynchronous decisions with time limits. Not every decision needs a meeting. Most decisions need someone to make a proposal, give relevant parties a day to object or modify, and then proceed unless there’s a substantive objection. This is not skipping the process. It’s running the process efficiently. The meeting is for decisions where the conversation is the work — where the back-and-forth genuinely produces a better outcome than sequential async input would. Most decisions are not like this, and defaulting to a meeting for them is a choice that distributes the waiting across everyone who attends.
The compounding effect of fast
Everything above is about removing friction. The case for doing it is usually made in terms of cost reduction: less time waiting means more time building, which means more gets done.
That’s true, but it undersells the argument.
The more important effect of short cycle times is the quality of the feedback loop. When you ship something and find out within an hour whether it worked, you learn faster than when you ship something and find out in two weeks. You make better decisions because you’re making them with recent information. You build more confidence in your understanding of the system because you’re testing your understanding constantly rather than periodically.
A team with a two-hour cycle time is not just doing the same work faster than a team with a two-week cycle time. They’re doing different work, because they’re informed by feedback that the slower team hasn’t received yet. The faster team makes a change, sees the effect, corrects course, makes another change — all before the slower team has shipped their first change of the week. The faster team’s decisions are not just more recent. They’re better, because they’re built on a larger base of observed reality.
This is the compounding effect that makes deployment frequency a leading indicator of engineering quality rather than just engineering speed. Teams that deploy frequently are not teams that are reckless about production — the best data we have, from the DORA research and from direct observation, is that teams that deploy more often have fewer production incidents, not more. The discipline of continuous deployment builds the systems and the habits that make each individual deployment safer.
The meeting that should have been a deploy is not just an efficiency problem. It’s a learning problem. Every step in the path between decision and production is a step where you’re not yet finding out whether you were right.
Shorten the path. Find out faster. Build on what you learn. That’s how teams stay fast, and it’s how fast teams stay good.