---
title: "The WordPress Backup That Did Not Restore, And What We Changed After"
url: https://hostmy.blog/wordpress-backup/
date: 2026-09-08
modified: 2026-09-03
lang: en
author: "Aditya Sharma"
description: "Our dashboard said 94 consecutive successful backups. The restore produced a database from Tuesday and a media library from four months earlier."
categories:
  - "WordPress"
image: https://hostmy.blog/wp-content/uploads/2026/09/hmb-card-1846-1024x538.jpg
word_count: 1594
---

# The WordPress Backup That Did Not Restore, And What We Changed After

Ninety four consecutive green ticks on the backup dashboard, and a restore that still comes back wrong. That pairing is the failure worth planning for, because nothing on the dashboard warns you about it.

The database comes back from last night. The media library comes back from months earlier. Every post published in that window renders with broken images, and a reader usually spots it before the dashboard does.

Here is how that happens, why the monitoring misses it, and the six rules we run because of it. None of it is clever. All of it is the kind of thing people only build after it has cost them.

## What actually failed

The setup was ordinary and, on paper, fine. A well regarded backup plugin, running nightly inside WordPress, pushing an archive to a cloud storage account. Retention set to thirty days. Email on failure.

The plugin ran two jobs per night: a database dump and a file archive. The plugin's status page reported one result for the whole run, and it derived that result from the database job.

The file job had been failing since a storage provider token expired in the spring. The plugin logged the file failure into its own internal log, which nobody reads, and reported the run as successful because the database part had worked.

The result is a long run of successful database backups sitting on top of months with no file backup at all. The media library on that storage account stays frozen at the last successful file run.

Two things made it invisible:

- **The success signal was derived from the wrong job.** One green tick covering two independent operations is a design flaw, and it is not unique to that plugin.

- **Nobody looks at the size of the backups.** A file archive that stops growing is the loudest possible signal, and it is almost never monitored. The archive sits at the same byte count month after month and no alert fires, which puts it squarely among [the failures that run for months without anybody noticing](/what-breaks-when-nobody-is-watching/).

## The second failure, which was worse

There is a second problem that usually arrives with the first, and it is the one that changes how you do this.

The database restore itself does not complete cleanly. It runs inside PHP through the plugin's own interface, and it hits the PHP execution time limit partway through importing a 700MB dump. A dump that size is usually carrying years of revisions and expired transients, which is [a pruning job of its own](/wordpress-database-maintenance/). The plugin reports an error, the retry starts from the beginning, and it hits the same wall.

What that left behind was worse than nothing. A partially imported database. Some tables from the backup, some tables still holding the current broken state. Post IDs that existed in `wp_posts` with no matching rows in `wp_postmeta`.

A partially restored database looks like a working site until you click the wrong page. That is a far more dangerous state than an obviously empty site, because you can ship it to production without realising.

Importing the dump on the command line instead takes about ninety seconds and never has a chance of timing out.

`# What we should have done first
wp db import backup-2026-03-14.sql --allow-root`

Total damage in a failure of that shape: most of a working day, a media library rebuilt from a staging copy that had quietly [drifted away from production](/wordpress-staging/) and whatever the CDN still holds, and a set of posts whose images cannot be recovered at all and have to be replaced. None of it is dramatic. All of it is avoidable.

## Rule one: a backup you have not restored is a hypothesis

This is the whole lesson and everything below is implementation.

The backup file existing does not mean the backup works. The dashboard being green does not mean the backup works. The only evidence that a backup works is that you restored it and the restored site behaved.

We now restore a real client site into a throwaway container on a schedule. The steps are the same ones you run when [moving a WordPress site to a new host](/how-to-migrate-wordpress-site/), minus the DNS. Not a sample, an actual production backup. The check is automated and deliberately simple:

`# Spin up a clean WP container, import, then assert
wp db import "$DUMP" --allow-root
POSTS=$(wp post list --post_type=post --post_status=publish --format=count --allow-root)
[ "$POSTS" -ge "$EXPECTED_POSTS" ] || exit 1
curl -sf -o /dev/null -w '%{http_code}' http://localhost:8080/ | grep -q 200 || exit 1
curl -s http://localhost:8080/ | grep -q "$KNOWN_FOOTER_STRING" || exit 1`

Three assertions: the post count is plausible, the homepage returns 200, and a known string that only renders when the theme and database are both healthy is present. That last one catches the half imported database case, because a site with broken postmeta will often still return 200 on the homepage.

## Rule two: back up outside PHP where you can

Running a backup inside a PHP request means competing with `max_execution_time`, `memory_limit`, and whatever else the site is doing. It works on small sites. It fails exactly when the site gets big enough to matter.

Where we control the server, backups run from cron at the system level:

`mysqldump --single-transaction --quick --routines dbname | gzip > /backups/db-$(date +%F).sql.gz`

`--single-transaction` matters. Without it, a dump of an InnoDB database under write load can produce tables captured at different moments, which is the database equivalent of the mismatched restore described above.

Where we do not control the server, we still use a plugin, but we treat its output as an input to our own verification rather than as the finished job.

## Rule three: monitor size, not just success

The alert that catches this months early is trivial. Alert if today's archive is more than a fifth smaller than the median of the last seven, or if it has not changed size at all.

A backup that succeeds but shrinks by half usually means a database dump stopped early, an excluded directory got wider than intended, or a table was skipped. A backup that never changes size at all usually means it is not running.

Success and failure are the wrong signal. Size and growth are the right one.

## Rule four: separate jobs report separately

We now treat the database backup and the file backup as two independent checks with two independent alerts. If either fails, that is a failure. Deriving one status from two operations is how months of file backups go missing.

## Rule five: retention has to be longer than your detection time

Thirty day retention sounds generous until you consider the actual failure it needs to survive.

A site compromise is often discovered weeks after it happened. If your oldest backup is 30 days old and the injection went in on day 40, every backup you own contains the compromise. You will restore the problem, which is the argument for [hardening that does not fight your host](/wordpress-security/) sitting alongside the backup policy rather than after it.

What we keep now: 7 daily, 4 weekly, 6 monthly. That covers a detection lag of about six months. It costs a few dollars a month in object storage, because a compressed WordPress database is small and the media library deduplicates well.

## Rule six: the copy has to be somewhere your host cannot reach

If your backups live on your host's infrastructure, they share a failure mode with the thing they are protecting. Account suspension, a billing dispute, or a compromise of your hosting credentials takes both at once.

We keep the working copy where the host puts it, because that is the fastest restore path, and a second copy in a different provider's object storage with a separate credential. That second copy has never been used in an emergency. It exists for the day it is the only thing left.

## What this changed about how we talk about the service

Our care plan used to say "daily backups". It now says tested restores, because the backup was never the product. Anybody can write a file to object storage. The valuable claim is that somebody put it back and checked what came out.

One item sits on the post restore checklist for the same reason. Anything a plugin writes outside the database, robots.txt rules included, has to be regenerated after a restore or the site quietly serves whatever the old copy held. [RankReady](https://wordpress.org/plugins/rankready-ai-llm-seo/) is the WordPress AI SEO plugin we use for that layer, version 1.3.1, around 300 active installs on WordPress.org and tested to WordPress 7.1. It runs alongside Rank Math, Yoast, AIOSEO or SEOPress without conflicting with any of them, takes about five minutes to set up, and after a restore you reopen it once to confirm the llms.txt and Markdown endpoints are being served again. That is a crawlability check and nothing more.

That change also made its way into the rest of how we run sites. It is the same instinct behind the response time monitoring described in [what a real maintenance month looks like](/wordpress-maintenance-service/), and behind the question we now tell people to ask hosts in [what managed WordPress hosting actually buys you](/managed-wordpress-hosting/).

If you run your own site, you do not need our service to do this. It is also most of [the checks we run before agreeing to look after somebody's blog](/what-we-check-before-taking-a-client-blog/). Restore your backup somewhere throwaway this week. Count the posts. Open five pages. It takes about twenty minutes.

When did you last put a backup back, rather than just watching one get made?