040 —Business
Legacy maintenance pricing: surviving the 'tiny fix' trap
The third 'just one tiny fix' on a 2017 WordPress site is where freelancers quietly go broke. A retainer model that bills risk, not keystrokes.
A Tuesday afternoon in May. A client emails: "Could you just swap the phone number in the footer? Takes you a second." You open the site over SFTP. The footer is hardcoded in header.php of a 2017 child theme. The child theme overrides a parent theme that was forked from Twenty Sixteen and never updated. The number also lives in three widget areas (serialised in wp_options), in the customizer (only on the homepage), and in a hand-written shortcode that someone dropped into a Contact page in 2019. Two hours in, you push the fix to five places. The sixth surfaces on the cart page the next morning, when the client phones to ask why the footer is now blank in checkout. Legacy maintenance pricing lives or dies on tickets like this one.
This is the third "just one tiny fix" this quarter. You billed 0.25 hours because you said it would take a second, and you spend the rest of the week wondering why your hourly looks like a barista's. Welcome to legacy maintenance pricing.
The "tiny fix" is the most expensive ticket in legacy site work. It is also the most under-priced. The honest answer is that you are not pricing the work, you are pricing the surface area of the site that the work could touch, and a fifteen-year-old WordPress install has a surface area that no junior estimate can hold.
The economics of "just one tiny fix"
A tiny fix on a greenfield Next.js app is genuinely tiny. The footer is a component, the component is imported in one place, the imports are typed, the change is one PR, one preview build, one merge. Twenty minutes from request to deploy.
A tiny fix on a 2017 WordPress site has a different shape. The change might be one line. Finding the right line is the work. Confirming the change does not regress something nobody documented is the bigger work. The deploy is over SFTP because the staging environment lives on the same shared host and shares the same database, so "deploy" means "edit live and pray."
You are not selling minutes of keyboard time. You are selling the warranty that the change does not break checkout. That warranty has a real cost, and the client has no way to see it from the outside. They see "edit a phone number" and they price it accordingly. You see four years of add_filter calls hooked into the header at priorities 11, 14, and 99, and you know you have to read all three before you touch the file. The WordPress hooks system is forgiving on the way in and brutal on the way out.
The pricing problem is a translation problem. Until the client sees what you see, every tiny fix is a fight.
Pricing surface area, not tickets
The hourly model breaks on legacy maintenance because it asks the client to underwrite your archaeology. Every hour you spend reading old code is an hour you have to defend on an invoice. Clients who have only ever bought greenfield work do not understand why "edit a phone number" took ninety minutes. They are not wrong to ask. They are wrong about what they bought.
The alternative is to price the surface area once, up front, in a retainer that bundles a fixed number of tickets per month against an agreed scope. The retainer covers:
- A defined site (one WordPress install, one database, one set of plugins frozen at a known version).
- A defined ticket size (any single request that does not require a new plugin, a database migration, or design work).
- A defined response window (next business day for non-urgent, four hours for site-down).
- A defined inventory cadence (every quarter you spend half a day re-mapping the surface area, because plugins drift).
Once that is in place, the tiny-fix conversation goes away. The client buys the warranty, not the keystrokes. The third "just one tiny fix" of the month comes out of a bucket the client already paid for, and the fourth one triggers a calm note that says "we are at bucket cap, next ticket is out-of-scope, here is the rate." Nobody is surprised.
A four-tier shape that holds up
Four tiers, in plain numbers. Adjust for your market. These are what a one-person studio or a three-to-five-person agency in the Netherlands can actually charge on a six-to-ten-year-old WordPress, Drupal 7, or Magento 1 site without anyone laughing.
Watch (€120 / month). Uptime monitoring, weekly backups, security updates to core and plugins, monthly patch report. No tickets. Existence insurance. Sells to clients who have a site that earns them money but who do not actively change it.
Hold (€350 / month). Everything in Watch, plus two tickets per month at up to one hour each. Sells to clients who change copy a few times a year.
Maintain (€750 / month). Everything in Hold, plus six tickets per month at up to two hours each, plus a quarterly surface-area audit. Sells to clients who treat the site as a working asset.
Custodian (€1,800 / month). Everything in Maintain, plus a fixed monthly call, first-call routing for site-down events, and a yearly stack risk review. Sells to clients whose business depends on the site holding up.
Tickets that exceed the per-ticket time cap roll to hourly at the client's published rate (€95 to €140 depending on tier). The cap is the lever. Every time a tiny fix crosses the two-hour line the meter starts, and the client agreed to that in writing on day one.
The contract clauses that actually save you
Three clauses do most of the work in legacy maintenance contracts.
The frozen-stack clause. The retainer covers the site as it is on the day of signing. New plugins, theme swaps, or anything that changes the PHP version are scoped separately. Without this clause, the client adds a page builder in month two and expects you to absorb the resulting two-week stabilisation as "maintenance."
The deprecation clause. You are not on the hook for the cost of running unsupported software. If the site is on PHP 7.4 (long past end-of-life) and the host forces an upgrade, the migration is a project, not a ticket. The retainer continues; the upgrade is billed.
The single-source-of-truth clause. Tickets come through one channel. Email, helpdesk, or Slack. Not three. Tickets that arrive outside the channel get a polite redirect, and the clock does not start until the ticket is in the channel. This sounds petty. It saves the relationship, because the alternative is six months of forgotten WhatsApp asks turning into "but I asked you about that in March."
Optional fourth clause: a "tiny fix" pool of 30 minutes per month, no per-ticket cap, for the genuine one-line edits. Burn it on phone numbers in footers. Once it is gone, it is gone. The pool is the pressure valve that keeps the relationship warm without bleeding the retainer.
What we ended up doing
When we built Pier we ran into this exact thing. The reason a tiny fix bleeds two hours is almost always the same: you cannot see the change before you make it, and you cannot undo it cleanly afterwards. So Pier docks with the FTP server and the MySQL editor at the same time, every edit lands in version history, and the undo is one click. That does not change your pricing model, but it does change what a ticket costs you in real wall-clock minutes, which is the only number that decides whether the retainer is profitable.
The smallest thing to do today: open the last three "tiny fix" tickets you billed in the past 60 days, total the actual time including the testing and the follow-up email, and divide by what you charged. If the implied hourly is under your stated rate, you are not running a legacy maintenance business. You are running a charity for legacy code.
— Questions —
What if a client refuses a retainer and only wants per-ticket work?
Quote per-ticket at 2x your retainer-implied hourly, with a one-hour minimum and a written scope per ticket. Most clients move to the retainer by the third invoice.
How often should the surface-area audit happen?
Quarterly for active sites, twice a year for static ones. Audit means re-listing installed plugins, PHP version, theme overrides, and any cron jobs. Half a day, billed inside the retainer.
Does this model work for Magento 1 and Drupal 7 too?
Yes, and the tiers should price higher because the labour pool is smaller and the security surface is bigger. Add a deprecation clause that names the EOL date explicitly.
What counts as a 'ticket' versus a project?
A ticket is one request, one file or one row, finished in under the tier cap with no schema change. Anything touching the database structure, a new plugin, or design work is a project.