Scope creep is a contract problem, not a communication problem
Being clearer about the scope does not stop it changing. A written change process does.
TL;DR
I spent two years treating scope creep as a communication failure: write a clearer scope, restate it in kick-off, confirm in writing. The scope still grew, because the requests were reasonable and I had no mechanism for pricing them other than a conversation I did not want to have.
The fix was a clause and a template. Every out-of-scope request gets a one-page change order with a day estimate and a signature line, sent within a day. Nothing about my communication changed; the awkward conversation became a form.
Over eleven engagements since, 23 change orders have been raised, 17 signed, 6 declined by the client — and the declined ones are the clearest evidence it works, because previously I would have built all six for free.
The case this post is about
A four-week engagement: build an offline sync layer for an existing app. A written scope, agreed:
## Scope
- A sync queue persisting pending operations (create, update, delete)
- Conflict resolution: last-write-wins on a per-field timestamp
- Retry with exponential backoff and jitter
- Unit tests for the queue and the resolver
## Out of scope
- UI changes
- Server-side changes
- Migration of existing local data
Clearer than most of my contracts, with an explicit out-of-scope list.
The symptom
Six requests over four weeks, each arriving as a sentence in a Slack thread:
week 1 "could the sync show a little spinner while it's running?" ~0.5 days
week 2 "the conflict thing — can it ask the user which version to keep?" ~3 days
week 2 "we'll need this to work for the new Orders endpoint too" ~1.5 days
week 3 "can you add a 'retry now' button?" ~0.5 days
week 3 "while you're in there, the existing local DB needs migrating" ~2 days
week 4 "QA wants a debug screen showing the queue contents" ~1 day
────────
8.5 days
Two of those are explicitly on the out-of-scope list ("UI changes", "migration of existing local data"). I built all six.
Why I built them, which is the honest part:
"could the sync show a little spinner" → half a day. Refusing over half a day
felt petty, and I wanted the reference.
"ask the user which version to keep" → three days, and by then I had already
said yes to two smaller things, so saying
no to this one needed me to explain why
this was different. I could not, cleanly.
That second line is the mechanism: each yes made the next no harder, because I had established that out-of-scope work happened without a price.
The outcome:
contracted 4 weeks @ £2,000 = £8,000
actual 6.7 weeks
unbilled 2.7 weeks ≈ £5,400
client's view delighted; referred me twice
my view a 33% discount I never agreed to
The client was not exploiting me. From their side, they asked and I said yes, which is what a cooperative supplier does.
Why the obvious thing did not work
"Write a clearer scope." Mine had an explicit out-of-scope list, and two of the six requests were on it. A clearer document does not help when the problem is that I will not invoke it.
"Restate the scope at kick-off." I did. The requests still came, because they were reasonable requests from someone discovering what they needed. Restating boundaries does not make a person stop having new requirements.
"Just say no." This is the advice everyone gives and it is not a mechanism. Saying no to "a little spinner" in a Slack thread costs goodwill I need, and the asymmetry — half a day of my time against a slightly awkward message — makes yes the locally rational choice every time.
"Track the extra time and raise it at the end." I tried this on the next project. Presenting 2.7 weeks of accumulated extras in week six is a much worse conversation than pricing half a day in week one, and the client reasonably asked why I had not mentioned it sooner.
The actual mechanism
Scope creep is not a failure of clarity. It is a missing process for a normal event.
Three facts make it inevitable without one:
Requirements genuinely change. A client discovering, in week two, that users will need to choose between conflicting versions is a good outcome — they learned something. Treating that as bad behaviour is wrong.
Each individual request is small. Half a day, one day. Small enough that pricing it feels disproportionate, which is exactly why the total accumulates unseen.
And the cost of refusing is social, immediate and borne by me; the cost of agreeing is financial, deferred and invisible. Every incentive at the moment of the request points at yes.
So the fix cannot be willpower or clarity. It has to change what the default is.
Written as a decision procedure, the thing it removes is the in-the-moment judgement call — the request is classified against the signed scope, and the classification determines the reply:
from dataclasses import dataclass
@dataclass
class Request:
summary: str
estimate_days: float
covered_by: str | None # the signed scope line it falls under, if any
def respond(req, signed_scope, budget_remaining_days, day_rate):
# Genuinely in scope. Just do it — a process that haggles over covered work is one
# you stop using by week three, and then it protects nothing.
if req.covered_by and req.covered_by in signed_scope:
return f"in scope ({req.covered_by}) — proceeding, no change note"
cost = req.estimate_days * day_rate
# The critical branch: SMALL changes are the ones that accumulate, so they are
# priced too. Waiving them "because it's only half a day" is the exact mechanism
# that produced three unpaid weeks, one reasonable yes at a time.
note = (f"CHANGE: {req.summary}\n"
f" +{req.estimate_days}d · {cost:,.0f} · not covered by signed scope\n"
f" schedule impact: +{req.estimate_days}d on delivery")
if req.estimate_days > budget_remaining_days:
note += (f"\n NOTE: exceeds remaining budget ({budget_remaining_days}d). "
"Needs a decision on what comes OUT, or a budget increase.")
# Never auto-accept. The output is a note requiring a countersignature, because the
# deliverable here is a DECISION BY THE CLIENT, not a faster yes from me.
return note + "\n → proceeds on written approval"
On the named case — the version-conflict request in week two:
>>> respond(Request("Let users pick between conflicting versions", 2.5, covered_by=None),
... signed_scope=SCOPE, budget_remaining_days=1.0, day_rate=650)
CHANGE: Let users pick between conflicting versions
+2.5d · 1,625 · not covered by signed scope
schedule impact: +2.5d on delivery
NOTE: exceeds remaining budget (1.0d). Needs a decision on what comes OUT, or a budget increase.
→ proceeds on written approval
Nothing in that output refuses the work, which is the point — the client learned something real in week two and the right answer was to build it. What changed is that the 2.5 days became visible at the moment of asking, to the person who could decide whether they were worth it.
no process: a request → a decision I make under social pressure → usually yes, unpriced
a process: a request → a form with a number → a decision the CLIENT makes
The change order moves the decision to the person who holds the budget, which is where it belongs — and removes me from the position of having to argue that my time has a price.

Clarity does not stop scope changing; it only tells you that it has. What stops the unbilled work is a process that makes pricing routine — so the decision moves to the client and no conversation is required.
The fix
A clause, a template, and a rule about timing.
1. The clause, in every contract:
## Changes
Work outside the Scope section — including work listed under Out of Scope — is a
Change Order.
A Change Order is a written estimate in days at £400/day, sent within one business
day of the request, and begins only when you approve it in writing. Either party
may decline; declining pauses only the affected item.
Requests of half a day or less are accumulated and raised together once they total
one day, so neither of us spends time on paperwork for small items.
That last paragraph is the part I added after six months, and it matters: without it, pricing a half-day spinner is more friction than the work, which is how the first yes happens.
2. The template, which takes about four minutes to fill in:
# Change Order 3 — user-facing conflict resolution
Project: Offline sync layer · Date: 2026-11-19 · Requested by: Priya (Slack, 18 Nov)
## What was requested
When a sync conflict is detected, present both versions to the user and let them choose,
rather than resolving automatically by last-write-wins.
## Why this is outside the current scope
The agreed scope specifies automatic resolution (last-write-wins on a per-field
timestamp) and lists UI changes as out of scope. This request requires a UI surface
and changes the resolver's contract.
## Estimate
resolver returns a conflict instead of resolving 1.5 days
a conflict-presentation API for the UI layer 0.5 days
a reference UI (one screen, unstyled) 1.0 day
────────
3.0 days @ £400 = £1,200
## Effect on the schedule
Delivery moves from 6 Dec to 11 Dec.
## Decision
[ ] Approve [ ] Decline [ ] Discuss
Signed: ______________________ Date: __________
Four properties:
It states why it is out of scope, quoting the agreed document. That sentence does the work my awkward Slack message used to do, and it does it neutrally.
It names the schedule impact, which is often what the client actually cares about — twice a client has approved the cost and asked to defer the item to protect the date.
"Discuss" is an option. Without it, the form feels like an ultimatum; with it, several change orders have turned into a smaller, cheaper version of the request.
And it goes out within one business day. This is the rule that matters most. A change order sent a week later reads as a complaint; one sent the next morning reads as process.
3. And a running tally in the weekly update, so nothing accumulates invisibly:
## Week 2
Done: queue persistence, retry with jitter, 31 tests
Next: conflict resolver
Change orders: CO-1 approved (£200, spinner), CO-2 pending (£600, Orders endpoint),
CO-3 sent today (£1,200, user-facing conflicts)
Schedule: on track for 6 Dec, +2 days if CO-2 and CO-3 are approved
How this turned out
Eleven engagements since:
change orders raised: 23
approved: 17 (£14,800 billed that previously would have been free)
declined by the client: 6
turned into something smaller: 4 (of the 17, after a "Discuss")
raised more than 1 day late: 1
The six declines are the strongest evidence. Every one is work I would previously have built unbilled — and the client, given a price, decided they did not want it. That is not me extracting more money; it is the request being evaluated at all.
One of the declines, verbatim-ish:
CO-5: a debug screen showing queue contents (1 day, £400)
client: "not worth £400 — can QA just read the DB with a SQLite viewer?"
outcome: declined. I sent them a 3-line note on how to pull the DB off a device.
Fifteen minutes instead of a day, because somebody with the budget evaluated the trade.
The commercial effect:
before (2 yrs) after (11 engagements)
unbilled overrun ~25% of hours ~3% of hours
client satisfaction high high (2 of 11 mentioned the process positively
in a reference; 0 negatively)
engagements where the
relationship soured 1 0
And the one late change order, because it is the failure mode to watch:
CO-19 raised 5 days after the request. The client had already told their
stakeholders it was in progress, so declining was awkward for them.
Approved, and it was the only change order that produced a tense conversation.
Timeliness is the whole mechanism. A change order raised late is the old problem with extra paperwork.
One honest limit: this works with clients who have a budget and authority to approve small amounts. Twice I have worked with someone who had to escalate every £400 decision, which made the process slow and annoying for both of us — there, the better shape is a pre-approved change budget ("£2,000 of changes at your discretion") agreed at the start, which I now offer.
What this generalizes to
A recurring problem with no process is a process problem. If a situation arises on every project and is handled by judgement each time, the judgement is the bug. Write the process once.
Move a decision to whoever holds the trade-off. I was deciding whether my time was worth spending on a request, which is the client's decision — they have the budget and the priorities. A form with a number puts it where it belongs.
Make the right action lower-friction than the wrong one. Saying no was socially expensive; sending a form is four minutes. Changing the relative cost of the two options did what two years of resolving to be firmer did not.
Timeliness converts a complaint into a process. The same document is routine on day one and accusatory on day seven. Any mechanism that depends on raising something must also specify how quickly.
Related
- The fixed-price project that cost me three weeks of unpaid work — where the clause came from
- The rate conversation I kept getting wrong — pricing the engagement itself
- The code review that should have been a conversation — another threshold with a named action
Keep reading
This is part of Freelancing & consulting for engineers — the commercial side of the work, written from the mistakes. Follow MobileByteSensei for the rest.
One post a week, no digest
New writing when it lands. No roundups, no course pitch, no "10 tips".