Case study: A membership and online-course business
Rescuing a slow membership site, then moving it without losing an order
Logged-in pages took 6 to 7 seconds and three people had offered three different reasons. The real cause was measured, fixed, and the site moved to hosting the owner controls in a five-minute window.
A membership and online-course business came to me with a site that took 6 to 7 seconds to load any page a member saw. Three people had already offered three reasons. Too many plugins. A bloated database. An old version of PHP.
None of them was the main problem. I found that by measuring, and I disproved eight confident diagnoses along the way. Two of those were mine.
- Public page load~8 s to ~350 ms
- Member page load6-7 s to 2.0-2.5 s
- PHP cache hit rate0.28% to 96.88%
- Database queries for one menu47 to 1
What was actually wrong
The site shared a server with three other businesses. That part is normal. The problem was one level down: all four sites shared a single PHP code cache, and it was far too small for the four of them.
| Measure | Shared server | Isolated copy |
|---|---|---|
| Cache hit rate | 0.28% | 96.88% |
| Time spent recompiling code, per request | about 2.6 s | close to none |
So the server was recompiling nearly the whole site on almost every request. You can't fix that on a shared box. The cache is sized once for everyone, and restarting it restarts everyone.
I didn't ask anyone to take that on faith. I rebuilt the fix on a throwaway server first and showed it working before proposing any move.
Quick wins first, on the old server
A hosting decision takes weeks. The owner shouldn't wait that long for anything to improve, so I fixed what I could where the site already lived.
- A booking plugin started a session on every page, which made every page uncacheable. Removing that on public pages took them from about 8 seconds to about 350 ms.
- A membership plugin ran 47 database queries to build something that needed 1. I fixed it and checked the output matched across all 23 taxonomies.
- Eight simultaneous editor requests dropped from 15.97 s to 7.67 s.
- Settings loaded on every request shrank from 2,357 KB to 1,323 KB.
- A 6.8 GB debug log, still growing, got deleted.
Saying no to the expensive answer
A premium managed WordPress host was on the table. I priced it properly and recommended against it in writing. The tier this site needed was $700 a month. A dedicated server with the same specs was $170. That's about $6,400 a year for hosting that wouldn't have been meaningfully faster, because the host was never the problem.
The site moved to a dedicated server on an account the owner controls. Same size as before. The gain was having it to itself.
Changing one thing at a time
The original plan bundled the move with a PHP upgrade. I split them apart, and I think that was the most useful decision of the whole project.
The upgrade was worth about 6%. The dedicated cache was worth about 2.6 seconds per request, and it didn't need the upgrade. Bundling them would have meant proving every checkout and course flow on a new PHP version before anyone could set a date. The outgoing agency also told me an earlier upgrade attempt had blanked the site. So migration night changed one thing: the server.
Finding out what the owner actually owned
The outgoing agency cooperated throughout, and I still checked every statement against the site's own database. That turned up things nobody knew to ask about.
- The booking suite, the most expensive piece on paper, was a lifetime license already registered to the owner. Renewal cost: $0.
- Ten paid plugins had license records and no installation.
- The chat widget was a sub-account inside the agency's marketing platform, with the branding stripped off. It would have died quietly the day that account closed.
- Every order confirmation and password reset went out through a cloud account belonging to the agency's principal.
I also caught a false alarm of my own. A course plugin looked unlicensed, but the "invalid" flag came from a test copy where I'd deliberately blocked outbound traffic. The real setting said valid. That one was a step away from reaching the owner as bad news.
The DNS problem, and designing around it
The plan assumed I could shorten the DNS cache time to 5 minutes before the move. The registrar's panel had no control for it, and the records were pinned at 4 hours. Moving to a different DNS provider wouldn't reliably help either, because the registry itself caches that change for 48 hours.
So I stopped trying to shrink the delay and made the delay harmless. The old site would be frozen, unable to take orders or signups. Anyone still reaching it during the switchover would see a maintenance page. Nothing they did could be lost.
Copying the DNS zone turned up 21 records, including three email authentication records the new provider's import scan missed. I compared every record against the live answers before switching.
Cutover night
Everything that could be done ahead of time was. The security certificate was issued in advance, so nobody saw a browser warning after the switch. Scheduled jobs were installed but switched off. Server settings were confirmed through a real web request, because the command line reported different values than the site actually used.
| Elapsed | Step |
|---|---|
| 0:00 | Old site frozen. User, order and post counts recorded |
| 1:36 | New files synced. Full database imported in 47 seconds |
| ~3:00 | Counts re-checked on the new server: exact match |
| ~4:00 | Mail and scheduled jobs switched on |
| ~5:00 | DNS switched. Public resolvers already answering |
Zero data loss was measured, not assumed. The counts recorded at the freeze matched the new server exactly, and all 516 of 516 database tables came across. Then I made a real purchase, with real money, and watched it go through end to end: payment captured, order completed, appointment booked, confirmation email in the inbox. I refunded it, which tested the payment path in the other direction.
The five-minute estimate came from a rehearsal. The first guess was 30 to 45 minutes. It took three practice runs to get the import clean.
After the move
| Metric | Before | After |
|---|---|---|
| Member pages | 6-7 s | 2.0-2.5 s |
| Public pages | ~350 ms | ~200 ms |
| PHP cache hit rate | 0.28% | 96%+ |
| Downtime for visitors | about 5 minutes | |
| Server errors, first 12 hours | zero |
One near miss, caught early. The new server's code cache had room for 16,229 files. Real traffic filled 12,181 of them within hours. Left alone, it would have started recompiling within days, a milder version of the original problem. I raised the limit to 65,536 before it mattered.
Cutting the last cords
Moving a site is the easy part. The services it quietly depends on are where handovers go wrong, because they fail weeks later instead of on the night.
Transactional email moved to a relay on the owner's own hosting bill: $0.80 a month for 8,000 messages. I picked that tier from the real mail log, 1,732 messages in 15 days, with headroom, because a full quota stops mail without telling anyone. The first setup passed every visible test. The raw headers showed the provider adding one-click unsubscribe links to order confirmations. A customer who clicked one would have stopped getting password resets. Fixed before it shipped.
The chat widget moved to the owner's own account. My first swap answered in live-chat mode instead of as a bot, so I reverted it within minutes. That was cheap only because I'd taken a backup first.
| Service | Before | After |
|---|---|---|
| Hosting | The agency's shared server | The owner's account |
| Domain and DNS | A registrar the owner couldn't log into | The owner's registrar |
| Transactional email | The agency principal's cloud account | The owner's account |
| Chat widget | The agency's marketing platform | The owner's account |
| Theme and plugin licenses | Four under agency accounts | All in the owner's name |
Nothing breaks when the previous vendor closes their accounts. That's the actual deliverable. The speed is what people talk about.
Things nobody asked me to look for
- A 5.3 GB full backup of the site, database included, sat in a public folder at a guessable filename. Removed.
- A folder of paid downloads lost its access protection in the move, because the new server ignores the old rule file by design. A deliberate test caught it and the rule now lives in the server config.
- A lead form had been silently broken for three weeks. It showed "check your inbox" whether or not the signup worked.
- Analytics counted every visit three times. One property, three tracking tags.
- Old credentials for services no longer in use were still sitting in the database.
What this doesn't prove
This is one site, and one night of post-move numbers at the time of writing. A week of data is still coming. The PHP upgrade and plugin cleanup are the next phases, and I've measured two of them on a copy (about 2.07 s and 1.70 s off the homepage) but not shipped them yet.
If your site sounds like this one, slow for reasons nobody agrees on, or tied to a vendor you're leaving, tell me about it. The first step is always measuring, and I'll tell you what I find even if the answer is that you don't need me.