Tag: engineering

  • How our Friday demo became a sales asset

    How our Friday demo became a sales asset

    Every Friday at 3pm, two engineers and one PM run a ten minute demo of what shipped that week. For a year it lived inside a private Loom folder shared with the rest of the team. Six weeks ago, we started publishing the recording to our customer-facing changelog.

    We did not plan for it to become a sales asset. That part surprised us.

    What the demo looks like

    It is not polished. We record on Zoom, trim the top and tail in Descript, and post the file next to the changelog entry in our docs site. The rules we set for ourselves are short:

    • Ten minutes hard cap. If we run over, we cut.
    • Show the feature working in a real workspace, not a demo tenant.
    • One engineer walks through the code path or the migration. The PM narrates why.
    • Mention the Linear ticket number on screen so viewers can search for context.
    • No slides.

    The first public recording covered a small change to how we render org charts. The engineer showed the diff on GitHub, then flipped to the browser and dragged a node around. Fifty-two seconds. That was the whole segment.

    How sales found it

    Two weeks in, a rep on our AE team dropped a link in Slack. She had forwarded that week’s demo to a prospect who kept asking whether we shipped fast enough to keep up with their internal roadmap. The prospect replied within an hour and asked to move the next call forward.

    Then it happened again. And again. By week four, three reps were routinely attaching the changelog demo to follow-up emails. One of them told us the video did something a case study could not: it showed the people building the product talking about a specific decision they made on a Tuesday.

    The prospect wrote back saying they wanted to work with a team that ships like this.

    What we changed after that

    We did not restructure the demo. We did tighten a few things:

    • We now include the engineer’s name and role on screen for the first five seconds of their segment.
    • We added a short written summary above the video in the changelog so readers can skim.
    • The PM tags the relevant Datadog dashboard when we talk about a performance fix, and shows the graph.

    The recording still takes forty minutes of prep and ten minutes to run. It is the same meeting we were already having on Friday. The only difference is that it is public now.

  • The day 1 onboarding checklist we finally trust

    The day 1 onboarding checklist we finally trust

    Our first attempt at onboarding was a Google Doc that nobody updated. Our second was a Notion database with fourteen owners and no owner. This is the third version, which has run through nine hires without a rewrite, and which our last four joiners have said was the first day at a job that felt planned.

    The rule we work backwards from: nobody sits idle. Not the new hire, not the buddy, not the manager. Every hour on day 1 has a defined output, and we can point at the output the next morning.

    The twelve items

    We track this as a Linear project cloned from a template. Each item is a ticket assigned to the person responsible, with a due time in London hours. If a ticket slips, it pages the hiring manager in Slack. Nothing here is aspirational; every item has failed at least once, which is why it made the list.

    1. Laptop imaged and on the desk by 08:30. IT owns this. If shipping is late, we send a loaner from the office by courier the night before.
    2. Accounts provisioned in Okta. Google, GitHub, Linear, Slack, Notion, Datadog, Figma, 1Password. Provisioned the Friday before, tested by IT with a throwaway login so we catch a missing SSO group before Monday.
    3. Buddy assigned and confirmed. Named two weeks in advance, blocked on the calendar for the whole day, not a stretch assignment for someone already underwater.
    4. Welcome standup at 09:15. Fifteen minutes with the immediate team. Names, what each person is shipping this sprint, one question the new hire wants answered.
    5. Dev environment running by 11:00. We keep a bootstrap script in the main monorepo that clones, installs, seeds the database, and runs the test suite. If it fails on a fresh machine, that is a P1 for the platform team.
    6. First PR opened by 14:00. Every team keeps a small backlog of tickets tagged good-first-issue. Copy tweaks, config renames, a missing test. The point is to touch the pipeline.
    7. First commit merged by end of day. The buddy reviews, the CODEOWNERS bot approves, CI runs green, and it deploys through our normal path. No ceremony, no exemption from the release train.
    8. Three intro coffees on the calendar. Not scheduled by the new hire. The manager picks three people outside the immediate team and books thirty minutes each across the first two weeks.
    9. Engineering handbook read. Sixty pages in Notion. It covers how we branch, how we write RFCs, how we run incidents. We ask two questions about it at the retro.
    10. Shadow the 15:30 standup for the sister team. We want new joiners to see how another team runs before their own becomes familiar.
    11. Post in #intros. A short paragraph and a photo of something the person likes. Not a policy, but a norm every hire has followed for two years.
    12. End of day retro with the buddy. Twenty minutes. What worked, what was confusing, what the new hire wants to change tomorrow. Written up in the Linear ticket and read by the manager the next morning.

    Why the first commit matters

    People argue this one with us. They say it puts pressure on the new hire, or that the commit is trivial and doesn’t prove anything. Both criticisms are fair, and both miss the point.

    The first commit is not a test of the new hire. It is a test of us. If a fresh joiner cannot ship a one line change on day 1, we have broken something: the bootstrap script, the CODEOWNERS file, the CI pipeline, the deploy access, or the buddy’s calendar. When we started measuring this, we found that on average two of those five were broken for any given hire. Now it is closer to zero.

    The first commit is a test of us, not of the new hire.

    The commit itself is often a typo fix in the marketing site or a colour token in Figma renamed in code. What matters is that the person walked the full path, saw the review comments, watched CI go green, and got the Slack notification when their change went live in production.

    What we cut

    The first version of this list had twenty six items. We cut fourteen. The pattern for cutting was consistent, and worth naming.

    • Anything that could wait until week two, waited until week two. Payroll paperwork, benefits enrollment, the security training module, the office tour with facilities. None of these help someone feel oriented on day 1.
    • Anything that produced no artifact came off. A “meet the CTO” slot without a follow up was noise. The CTO now joins the Friday demo instead, which is a real meeting with a real output.
    • Anything owned by more than one team came off. Shared ownership meant nobody chased it. Every remaining item has exactly one accountable owner, listed in the Linear ticket.

    We run this checklist every Monday and most Tuesdays. Nine hires in, the pattern holds. The buddy is tired by 17:00, the new hire is tired by 17:30, and the manager has a written retro to read on Tuesday morning. Nobody sat idle.

  • A take-home task that respects candidates

    A take-home task that respects candidates

    What we ask

    Our take-home is capped at two hours. We say two hours, we mean two hours, and we check on it. If a candidate spends four, we treat that as our fault for scoping poorly, not theirs for being slow.

    The prompt is a real problem we hit last March: a Linear webhook was dropping payloads under load, and our on-call channel in Slack filled with duplicate alerts. We strip the business context, sanitize the payload shape, and hand candidates a repo with the failing test already written.

    What we pay

    Two hours of senior engineering time is not free. We pay 200 pounds per completed submission, regardless of outcome. Finance flagged it as unusual the first time; now it runs through the same expense flow as contractor invoices.

    A short list of what the pay covers:

    • The two hours of coding.
    • The 20 minute walkthrough call afterward.
    • Any follow up questions we send over email.

    The rubric lives on GitHub

    Our scoring rubric sits in a public repo, versioned like code. Candidates read it before they start. We grade on four axes: correctness of the fix, quality of the test coverage, clarity of the pull request description, and whether the candidate flagged the ambiguities we planted.

    If you cannot show a candidate how you will grade them, you do not have a rubric. You have a mood.

    We do not hide the trick either. There is a race condition in the retry logic that most candidates miss. That is fine. Missing it is not disqualifying; pretending it does not exist in the walkthrough call is.

    Why bother

    The hiring market tells engineers their time is worth nothing until an offer lands. We think that assumption poisons the relationship before day one. When someone joins us on a Tuesday, they have already seen how we write tickets, how we scope work, and how we handle disagreement. Nothing about the first week is a surprise.

    Two candidates last quarter turned down our offer. Both cited compensation. Neither cited the interview. We will take that trade.

  • Why we stopped tracking hours

    Why we stopped tracking hours

    We killed the timesheet in March. Nobody misses it. What replaced it made our finance team nervous for about six weeks, and then it stopped making them nervous. Here is what happened.

    What we track now

    We stopped asking engineers, designers, and PMs to log hours against project codes in Harvest. It rewarded the wrong thing: presence. A person could sit on a Linear ticket for three days, log 24 hours to it, and produce nothing shippable. The timesheet said the work was funded. The product said otherwise.

    We now track two things per squad, weekly, in a Notion database that pulls from Linear and GitHub:

    • Shipped units of work: tickets that closed the loop, meaning code merged, feature flagged on for at least one real customer, and no rollback within seven days.
    • Cycle time: median days from “In Progress” to “Shipped” on Linear, per squad, broken out by ticket size (S, M, L).

    That is it. No story points. No hours. If a squad shipped four medium tickets in a week with a cycle time of 3.2 days, that is the artifact. We review it every Thursday in a 25 minute meeting called Ship Review.

    What finance pushed back on

    Our CFO’s first question was fair: how do we capitalize engineering costs for the R&D credit if nobody is logging hours to projects? HMRC wants a defensible allocation. Auditors want a paper trail. “The vibes were good on Thursday” is not a paper trail.

    Her second question was harder. If a contractor bills us for 40 hours and we have no internal hours to compare against, how do we know we are not being overcharged?

    What we told them

    We proposed a trade. Finance gets a defensible model. Squads keep their calendars.

    1. Every squad has a fixed roster, and every roster maps to one or two product areas in a Notion table. Payroll cost per squad is known. That is our allocation basis, reviewed quarterly.
    2. Contractors still log hours in Harvest, because they bill by the hour. Employees do not, because they do not.
    3. The Ship Review output feeds a monthly note to finance: shipped units, cycle time trend, and any squad where cycle time doubled without a headcount change. That flags stuck work faster than a timesheet ever did.

    Six months in, our R&D claim went through without a query. Cycle time on our payments squad dropped from 8.1 days to 4.4. Nobody has asked to bring hours back.

    A timesheet measures whether you showed up. A shipped ticket measures whether the customer got something.

    We know which one we would rather report on.

  • The debugging log we always regret losing

    The debugging log we always regret losing

    Every engineer on our team keeps a file called bug.md or scratch.txt or something equally forgettable. It lives in a folder that never gets checked into git. Nobody told us to make it. We all did anyway, because the third time a hard bug ate a full Thursday, we figured out that memory is the worst debugging tool we own.

    The file is append-only by convention, never by tooling. New entries go at the bottom with a timestamp and whatever we were poking at when the thought landed. When the bug closes, the log stays. When a laptop dies or we forget to sync it to iCloud, we mourn. One of us lost eight months of notes last October and still brings it up in retros.

    Here are three excerpts from the past quarter, lightly sanitized, that show why we would rather lose a Figma file than lose one of these.

    Excerpt one: the phantom timeout

    Tuesday, 2:14pm. Datadog spiked p95 on the checkout service to 8.2s. No deploys in 48 hours. Two engineers pulled in from a payments meeting.

    14:16 - checked prod dashboard, only /orders POST spiking, /orders GET fine
    14:19 - pulled last 500 traces, 91% share a redis GET on session token
    14:23 - ran KEYS scan against staging, 1.2M sessions cached from Friday load test
    14:27 - Marcus reminded me eviction policy on this cluster is noeviction
    14:31 - confirmed via redis-cli config get maxmemory-policy
    14:33 - shipping maxmemory-policy allkeys-lru now, PR #4821
    14:58 - p95 back to 340ms
    

    Six lines, one closed Linear ticket, one lesson written into our runbook. If we had captured a screenshot of the Datadog spike and nothing else, we would have a picture of the pain and none of the path out. Three months from now, when p95 climbs again on a Friday afternoon, this log will be the first thing we grep.

    Excerpt two: the flaky test that was not flaky

    This one ran across five days and two engineers. The Linear ticket collected fourteen comments. The log had forty entries. Only one of them mattered.

    Wed 10:04 - test_billing_webhook_signature failing on CI, passes local
    Wed 10:22 - retried 3x on CI, one green one red one red
    Wed 11:40 - added extra logging, deployed to CI branch
    Wed 14:15 - noticed failures happen only on runners pulled after 13:00 UTC
    Thu 09:30 - runner image was rebuilt yesterday, openssl bumped to 3.2
    Thu 09:45 - openssl 3.2 changed HMAC digest default padding behavior
    Thu 10:12 - our webhook signer never set padding explicitly, tests inherited default
    Fri 16:20 - PR #4903 pins padding, backports to test util
    

    The Wednesday 14:15 note is the whole thing. It is a stray observation, not a fix. If we had been operating inside a Slack thread and a screenshot pipeline, that observation would have been buried under seventeen emoji reactions and lost forever. In the log, it sits alone on a line, waiting to be reread on Thursday morning when it finally clicks.

    Excerpt three: the null we could not explain

    Mon 08:50 - user report: dashboard renders "customer name: null" for 4 accounts
    Mon 09:10 - all 4 accounts created between Aug 3 and Aug 5
    Mon 09:12 - Aug 3 was our migration cutover date
    Mon 09:14 - checked backfill script, filters WHERE created_at < '2025-08-03'
    Mon 09:15 - accounts created ON Aug 3 fell into the gap
    Mon 09:20 - one-off patch queued, wrote regression test, added to postmortem
    

    Twenty minutes, start to finish. A screenshot would have told us the UI was broken. The log told us we shipped a boundary bug in a migration script, and reminded us to check for the same class of mistake in the next migration on the roadmap.

    Why context beats screenshots

    Screenshots capture a state. Logs capture a decision tree. When we return to a screenshot, we see what we saw. When we return to a log, we see what we tried, what we ruled out, and what surprised us. The last of those is the reason we keep writing these files.

    A short list of what belongs in the log:

    • The exact command we ran, copy-pasted with its output
    • The question we were asking when we ran it
    • What we expected and what we got instead
    • Any stray observation that felt off but was not the bug
    • The commit hash, PR number, or Linear ID at the end

    A shorter list of what does not belong:

    • Long paragraphs
    • Screenshots
    • Anything we have to format before saving

    The rule that keeps the log useful is that it must be faster to add a line than to think about not adding it. The moment it feels like writing, it stops getting written. That is why nobody on our team keeps this file in Notion. Notion asks us to name the page, pick a template, and choose an emoji. By then the thought is gone.

    We have started nudging new engineers to keep one within their first week. Not as a policy. As a warning. On the day their laptop dies, they will remember which files they miss most. This is always one of them.

  • The two week feature flag rule

    The two week feature flag rule

    Most engineering blogs will tell you feature flags are free. Ship dark, roll out slow, keep the fallback path warm. We used to believe this too. Then we counted our flags.

    Last month we had 47 flags in LaunchDarkly. Nine were older than a quarter. Four were older than us remembering why they existed. Two of them fought each other in production and caused a payout retry loop that Datadog flagged at 2:14 on a Tuesday afternoon.

    Why the “keep it around, it’s cheap” argument is wrong

    The pitch for long lived flags is that they cost nothing. A boolean check, a config entry, a line in a dashboard. The real cost shows up somewhere else:

    • Every conditional doubles the state space a new engineer has to hold in their head when touching that file.
    • Test matrices grow multiplicatively. Two stale flags plus one new one gives you eight paths, and nobody writes eight tests.
    • Old flags rot silently. The useNewCheckout branch you shipped in March has drifted from the legacy branch it lives next to, and neither of you noticed.
    • On call gets worse. When something breaks, the first question is always “what flags are on for this customer?” and the answer takes twenty minutes to assemble.

    The flag was supposed to be a valve. Left open, it becomes a fork in the codebase you have to maintain twice.

    The rule we adopted

    Every flag has an owner and a fourteen day timer. At day fourteen, the losing branch gets deleted. Not deprecated, not marked for cleanup in Linear, deleted. If we shipped the new checkout behind a flag and it stuck, the old checkout code goes. If the rollout failed, the new code goes.

    The uncomfortable part: sometimes we delete a branch that turns out to be needed later, and we rewrite it. We accept that cost. A week of rework is cheaper than a year of dual maintenance.

    The mechanics are boring. Every Monday, a GitHub Action posts the list of aged flags to our #eng-hygiene Slack channel with the owner tagged. If the flag is still needed, the owner extends it once, by another week, and writes why in the thread. Second extensions get escalated to the Friday engineering sync.

    A flag that has been on for a month is not a flag. It is a feature you forgot to finish shipping.

    Since we started, our flag count has dropped from 47 to 12. Half the incidents we traced back to “unexpected flag interaction” have stopped happening. The rework tax has been real, and worth it.

  • How twelve of us run trunk-based development

    How twelve of us run trunk-based development

    We moved to trunk-based development eighteen months ago, when the team was seven engineers and the release train was groaning under its own weight. We are twelve now, split across three squads, and every one of us commits to main multiple times a day. What follows is the routine we settled into, and the two things that caught us off guard.

    None of this is theoretical. It is what we do between our Monday planning meeting and our Friday demo, using GitHub, Linear, Datadog, and a lot of small pull requests.

    The daily rhythm

    Our workflow rests on three habits that we practice without thinking about them now. When a new engineer joins, these are the three things we teach in the first week.

    • Feature flags before feature code. Every user-visible change ships behind a flag in LaunchDarkly. The flag lands in a separate PR, sometimes an hour before the feature work starts. That way the wiring is reviewed on its own, and the rollout is a config change rather than a deploy.
    • Branches that live less than a day. Our house rule is that a branch should not sleep. If the sun sets on your branch, you either open a draft PR to get eyes on it, or you merge whatever green subset you have behind a flag. We measure this: last quarter, the median branch age was 6 hours and 42 minutes.
    • Five-minute code review triage. At 10:15 and 15:15, whoever is on review rotation opens the GitHub PR queue and gives every open PR one of three responses: approved, one specific question, or a note that the review will land by end of day. No PR is allowed to sit without a signal for more than four hours during working time.

    The triage rule is the one that took the most discipline to adopt. Two twelve-minute windows a day sounds trivial, but it forced us to break up large PRs. If you cannot skim a diff in ninety seconds, the reviewer flags it and asks for a split. We settled on a soft cap of 400 lines of diff, and a hard cap of 800.

    What we do when things break

    Trunk-based development only works if main is trustworthy. Ours is protected by a required check that runs unit tests, a smoke suite against a staging environment, and a Datadog synthetic against three critical paths. When the check goes red, whoever pushed the offending commit has two options: revert within fifteen minutes, or roll forward within thirty. We track this in a Notion page called the Green Log. Since January, we have had eleven red-main incidents, and nine of them were resolved by revert.

    We optimise for the next commit, not the current one. A revert is not a failure. A stuck main is.

    What surprised us

    We expected the obvious wins: faster feedback, smaller blast radius, less time spent on release branches. Those all showed up. What we did not predict were two second-order effects that changed how the team behaves.

    Merge conflicts became rare. We assumed twelve people pushing to one branch would create a merge conflict every few hours. In practice, the opposite happened. Because everyone rebases against a fresh main multiple times a day, conflicts surface early, when the diffs are small and the context is still in someone’s head. Our GitHub metrics show a conflict rate of roughly one PR in forty, down from one in eight when we were running two-week release branches. The conflicts we do get are almost always resolved in under ten minutes by the author.

    Design conversations moved earlier. This is the change we care about most. When branches lived for a week, most technical debate happened at review time, when the code was already written and the author was invested. Now, because a branch cannot survive a day, engineers ask for a design opinion before they open the editor. Our #eng-design Slack channel used to see two or three threads a week. It now sees eight or nine, and most of them are fifteen-minute exchanges about approach, not aesthetics. A few times a month, one of those threads turns into a Figma sketch or a Linear ticket for a spike.

    We think the mechanism is simple. Short branches make the cost of throwing away code visible. If you have to justify a day of work, you tolerate risk. If you only have to justify two hours, you ask the question first.

    What we would tell a team starting today

    If we were setting this up again from scratch, we would do these things in this order:

    1. Put LaunchDarkly, or a homegrown equivalent, in place before you change any branching rules. Without flags, small merges are frightening.
    2. Write down the review triage times and defend them. Ours are on the team calendar as recurring events.
    3. Pick a diff-size cap and enforce it socially. Numbers matter less than the shared expectation that big PRs get split.
    4. Track red-main minutes as a team metric, not an individual one. We share the Green Log in our Friday retro.

    Trunk-based development is not a productivity trick. It is a set of constraints that make the team’s default behaviour better. Eighteen months in, we are shipping about twice as often as we used to, and the conversations we have about how to build things are earlier and more useful. Those two outcomes are worth more to us than any of the workflow mechanics that produce them.

  • Why our retros stopped finding the real problem

    Why our retros stopped finding the real problem

    The Friday retro ritual

    Every team we have worked on runs the same shape of retro. Sixty minutes on a Friday, three columns in a Miro board, sticky notes for what went well, what went poorly, and what to try next. Someone dot-votes. Someone else copies the top three items into a Linear ticket that nobody opens again. We used to run it that way too.

    In Q2 of last year, we shipped a release that took down billing for four hours. The retro landed the following Friday. The dot-vote surfaced “unclear on-call handoff” as the top item. We wrote a Linear ticket to rewrite the on-call runbook. Six weeks later, we shipped a different release that broke webhook delivery for two hours. The retro found the same category of problem, worded slightly differently, and produced another ticket that also went nowhere.

    The on-call runbook was fine. The problem sat upstream of it, and nobody in the room was willing to say so on a Friday afternoon with the person who owned the decision sitting three seats away.

    Why the standard retro fails

    Retros as most teams run them optimise for social comfort, not for truth. Three failure modes we kept hitting:

    • Recency bias. The team remembers Thursday’s deploy noise, not Monday’s design decision that set the deploy up to fail.
    • Consensus bias. Dot-voting rewards items several people already agree on, which selects for symptoms over root causes. Root causes are usually held by one or two people who saw them early and stayed quiet.
    • Performance bias. A live meeting is a stage. People tell the version of the story that protects the relationship, not the version that would help the next team.

    The billing incident showed us all three. The engineer who had raised a concern about the migration plan two weeks earlier did not repeat that concern in the room. Nobody wanted to spend the last hour of the week relitigating a decision that felt settled.

    The retro found something true and small. It missed the something true and large, because the format could not hold it.

    What we do instead

    We replaced the Friday retro with three artefacts, spread across the week. None of them takes more time than the meeting they replaced. We have run this shape for eleven months across two product squads and one platform squad.

    1. A written pre-mortem, filed on Wednesday

    Whoever owned the incident, feature, or sprint outcome writes a one-page pre-mortem in Notion. It is not a report of what happened. It is a written attempt to answer one question: if this failure repeats in six months, what will the story be? The author writes it alone, without review, and posts it in the squad Slack channel by end of Wednesday. It is time-boxed to forty-five minutes. Long documents mean somebody is hiding.

    2. A two-question survey, sent Thursday morning

    Every person on the squad, plus two adjacent stakeholders (usually a designer and a customer support lead), gets a Google Form with two questions:

    1. What did you see, hear, or think during this work that you did not say out loud?
    2. If you had a private ten-minute conversation with the person most responsible for the outcome, what would you ask?

    Answers are anonymised by the facilitator and pasted into the Notion doc under the pre-mortem. Response rate sits above ninety percent because the questions are specific and the form takes under five minutes.

    3. One blameless conversation, Monday at 10am

    The squad meets for thirty minutes on Monday. The pre-mortem and the survey answers are already in the room. The facilitator, who is not the tech lead, reads three or four survey answers aloud and asks the author of the pre-mortem to respond to them. No sticky notes. No dot-voting. No action items produced in the meeting itself. Proposals get added to the Notion doc during the following twenty-four hours, once people have had time to think.

    What changed

    The billing incident was one of eight retros we ran through the old format. The webhook incident was the ninth. Between month four and month eleven of the new format, we ran six retros across incidents of similar severity. Two produced Linear tickets that closed within a sprint. Three produced changes to how we scope Datadog dashboards before we ship, not after. One produced a decision to stop building a feature that two engineers privately thought would not land, and had not raised in a Friday meeting.

    The change is not that we find more problems. It is that the problems we find are the ones that matter. Three shifts explain most of it:

    • Writing before speaking gives people room to admit things they would not admit in a room.
    • Splitting the process across three days lets recency bias fade.
    • Removing the ritual of “action items produced in the meeting” removes the pressure to produce something visible, which is what pushes teams toward the easy, wrong answer.

    What we still get wrong

    The Monday conversation is fragile. If the facilitator lets it become a debate about the pre-mortem’s conclusions, it collapses back into the old format. We have had two of those in the last year. Both times, the survey answers that mattered most did not get read aloud, and the meeting ended with everyone agreeing on a symptom.

    We have also not solved the problem of what to do when the person most responsible for the outcome is the tech lead running the process. We rotate facilitation to a peer squad’s engineer in those cases, but it is not a clean answer.

    The retro, as most teams run it, is a meeting that produces the feeling of learning without the substance of it. If your Linear board carries three open “improve on-call handoff” tickets from three different retros, that is the signal.

  • The quarter we shipped no features

    The quarter we shipped no features

    Last October, three days after our Q4 planning offsite, we made a decision that felt reckless at the time. We were going to spend the entire quarter without shipping a single new feature. No new modules, no new integrations, no new dashboards. Only bugs, docs, and the internal tools our engineers had been asking for since spring.

    Our head of sales, Mira, found out on a Monday morning during our weekly go-to-market sync. She went quiet for about eight seconds, then asked whether we were serious. We were.

    What sales was worried about

    Mira had four deals in the pipeline that hinged on a specific promise: a Snowflake connector we had been talking about since June. Two of those deals were mid-market, one was a renewal expansion, and one was a competitive replacement worth around 180k in annual contract value. She pulled up the deal notes in Notion and walked us through each one.

    The fear was reasonable. If we froze features for 90 days, three things could happen:

    • Prospects would walk to competitors who kept shipping.
    • Existing customers waiting on requested features would churn at renewal.
    • The sales team would lose narrative ammunition on discovery calls.

    We agreed to review the freeze monthly. If any of those signals showed up in the data, we would call it off. Mira asked us to write down what “showed up in the data” meant, so we did: net revenue retention below 108%, gross churn above 1.4% monthly, or two consecutive weeks of stalled pipeline movement on flagged deals.

    What we shipped instead

    The engineering team split into three squads. One squad, which we called Fixit, worked exclusively through Linear tickets tagged with the “customer-reported” label. Another squad, Docs, sat with our support lead every Tuesday to identify the top ten most-hit help center pages and rewrite them. The third squad, Tooling, built the internal admin console engineers had been begging for.

    By week six we had closed 247 bugs, some of which had been open for over a year. The Datadog dashboard we cared about, the one tracking p95 API latency, dropped from 840ms to 310ms after two engineers rewrote a query planner in the reporting service. Our support team went from 34 open Zendesk tickets on any given Friday to 9.

    The internal admin console was the surprise. Before Q4, resolving a customer-reported billing issue took an engineer about 40 minutes: pull data from three tables, reconcile in a Google Sheet, patch, verify. After the tooling squad shipped the console, our support engineers were doing the same work in under 4 minutes. They did not need to page anyone.

    By the end of week ten, our on-call rotation had gone from one incident per shift to one incident every six shifts. Two engineers told me they were sleeping better. One of them had been talking about leaving.

    What happened to churn

    Here is the part nobody predicted. Gross churn went down. Not by a huge amount, but measurably: from 1.2% monthly at the start of Q4 to 0.7% by December. Net revenue retention held at 114%.

    Mira’s Snowflake deals: three of the four closed anyway. The connector question came up on discovery calls, and the answer we gave, which was that we were spending the quarter on reliability instead of new surface area, played better than we expected. One of the buyers, a VP of data at a healthcare company, told us he had never heard a vendor say that out loud. He signed in November.

    Two effects we did not model

    First, our NPS moved from 42 to 51. We got unsolicited notes in Slack from customer success managers whose accounts had stopped filing tickets. Second, our engineering hiring pipeline got healthier. Three candidates in December mentioned during their onsite loop that they had read our internal writeup about the freeze and wanted to work at a place that took reliability seriously.

    What we would do differently

    We got lucky on a few things and would not repeat every choice.

    1. We underestimated how disorienting the freeze would feel to product managers. Two of them felt sidelined for six weeks before we figured out how to give them meaningful work reviewing customer feedback and shaping the Q1 roadmap.
    2. We should have communicated the freeze to customers on day one, not week three. When we finally sent the note explaining what we were doing, the response was overwhelmingly positive. We could have banked that goodwill earlier.
    3. We did not set clear exit criteria beyond the churn and NRR thresholds. When Q1 planning arrived, some of us wanted to extend the freeze another month, and we did not have a decision framework for that conversation.

    We are not going to do this every quarter. Growth still matters, and a company that only fixes bugs is a company that gets displaced. But we now know the shape of what a deliberate pause looks like, what it costs, and what it returns. Next time we consider one, the conversation will be shorter, and the fear in the room will be smaller.

  • Velocity is a lie detector not a speedometer

    Velocity is a lie detector not a speedometer

    Every quarter we watch another engineering team roll a velocity chart into a review deck, gesture at the bars trending up and to the right, and declare progress. We used to do it too. Then we ran a small experiment on our own Velo delivery team: we hid the velocity number from three squads for a full quarter and gave them cycle time instead. Two of the three squads shipped more features. All three reported that planning felt less theatrical.

    This is not an argument against measurement. It is an argument against a specific metric that has quietly stopped telling us what we think it tells us.

    Velocity measures the ruler, not the road

    Story points are a ruler the team invented. When we grade a team on how many units of their own ruler they produce per sprint, we should not be surprised when the ruler starts stretching. We have watched this happen on our own boards. A ticket that would have been a 3 in April becomes a 5 by July. Nobody lies. Everyone remembers “that thing that turned out harder than expected,” and the estimate drifts up. The chart climbs. The output does not.

    Goodhart’s law shows up in the standup:

    When a measure becomes a target, it ceases to be a good measure. Velocity is the most polite example of this rule we have found in software.

    The other failure mode is subtler. Velocity averages hide the shape of the work. A team can hit 42 points every sprint for six sprints and be quietly falling apart, because 40 of those points come from a single engineer who is one Slack DM away from resigning. The bar chart cannot see that. Cycle time can.

    Cycle time is boring, which is the point

    Cycle time is the elapsed clock between “in progress” and “done.” It is boring because it measures reality rather than an estimate. We cannot inflate it by talking about it in a Wednesday grooming session. It refuses to care about our narrative.

    Here is the comparison we now put in front of every engineering lead we hire:

    • Unit. Velocity uses story points, a team invented currency. Cycle time uses hours or days, a currency everyone shares.
    • Gameable by. Velocity is inflated by re estimation, ticket splitting, and status theatre. Cycle time is inflated only by shipping faster, which is what we wanted.
    • Sensitive to. Velocity is sensitive to who is in the room during planning poker. Cycle time is sensitive to review queues, environment flakiness, and handoffs, the things that slow us down.
    • What it hides. Velocity hides bottlenecks behind an average. Cycle time exposes them by widening the tail of the distribution.
    • Actionable signal. A dropping velocity prompts a debate about commitment. A widening cycle time prompts a debate about the pull request that has been open since Tuesday.

    What we changed on our own board

    We run Linear for tickets and pipe every state transition into a Datadog dashboard. Once a week, on Thursday afternoon, we look at three numbers together:

    1. Median cycle time for tickets closed that week.
    2. The 90th percentile of the same distribution.
    3. The count of pull requests older than 48 hours.

    We deliberately do not look at velocity anymore. When a stakeholder in Notion asks “how much did we ship this quarter,” we answer with a count of shipped tickets and a link to the changelog. If they push, we show the cycle time trend. Nobody has pushed twice.

    The 90th percentile is the metric that changed our behaviour the most. Medians are polite. They hide the ticket that sat in code review for eleven days because the reviewer was on parental leave and nobody rerouted it. The 90th percentile has forced us to build a bot in Slack that pings the review channel every morning at 9:15 with any pull request older than a day. Our median moved a little. Our tail moved a lot.

    The objections we still hear

    Two objections show up in every conversation about this, and both deserve a real answer.

    The first is that cycle time punishes big work. A refactor that takes two weeks will show up as a fat cycle time number and drag the median. Our response: split the refactor. Not into fake tickets that ship in isolation, but into shippable, reversible steps. If a piece of work cannot be split, that is itself a finding, and the fat number is telling us something true.

    The second objection is that cycle time can also be gamed. Engineers can open tickets late, close them early, or keep everything in draft. Fair. We have seen all three. The difference is that these games are visible in the version control log and in the Linear activity feed. Velocity inflation is invisible, because the ruler itself is invisible. A gamed cycle time becomes a conversation. A gamed velocity becomes a slide.

    The metric we would keep if we could only keep one

    If a new engineering leader joined tomorrow and asked which single number to track, we would not hesitate. Track the 90th percentile of cycle time, week over week, and set a target for the tail rather than the average. That number is close enough to reality that people can argue about it usefully, and far enough from planning theatre that it does not warp under pressure. Velocity charts belong in a museum next to lines of code per day. We stopped drawing them and started drawing something harder to fake.