VPS Snaps

How to back up a WooCommerce store

A WooCommerce store is backed up like any WordPress site, a database dump plus wp-content, but its database changes with every order. Dump the whole database often, hourly or better, because a restore loses every order placed since the dump it comes from. Restore so new orders survive, and keep restored copies from charging cards, firing webhooks or emailing customers.

10 min readUpdated Checked against official documentation

What WooCommerce adds to a WordPress backup

The WordPress backup guide covers the database export, the file archive and the restore. WooCommerce adds these tables and folders on top (shown with the default wp_ prefix):

WhatWhere
Orders (High-Performance Order Storage)wp_wc_orders, wp_wc_order_addresses, wp_wc_order_operational_data, wp_wc_orders_meta
Orders (older stores)wp_posts and wp_postmeta, post type shop_order
Order line itemswp_woocommerce_order_items, wp_woocommerce_order_itemmeta
Order noteswp_comments, comment type order_note
CustomersWordPress users (wp_users, wp_usermeta), plus wp_wc_customer_lookup for Analytics
Saved payment methodswp_woocommerce_payment_tokens, wp_woocommerce_payment_tokenmeta
Scheduled jobswp_actionscheduler_actions, _logs, _groups, _claims
Webhooks, REST API keys, settingswp_wc_webhooks, wp_woocommerce_api_keys, wp_options
Product imageswp-content/uploads
Downloadable product fileswp-content/uploads/woocommerce_uploads
Logswp-content/uploads/wc-logs, or wp_woocommerce_log if logs go to the database

High-Performance Order Storage (HPOS) has been on by default for new stores since WooCommerce 8.2 (October 2023). Stores created earlier may still keep orders as posts, and with compatibility mode on, WooCommerce writes them to both. Check which one is authoritative; yes means the HPOS tables:

Terminal
sudo -u www-data wp --path=/var/www/shop.example.com option get woocommerce_custom_orders_table_enabled

One order's data spans many of these tables, and Action Scheduler holds jobs that refer to orders. Dump and restore the whole database together; never restore a few tables from an older dump. Keep woocommerce_uploads in the file backup too: those are the files customers paid for, and they are not in the Media Library.

Why backup frequency matters more for a shop

A blog that loses a day loses a post or two. A shop loses orders that were paid for. Take a store with 240 orders a day, about 17 an hour between 08:00 and 22:00, and a nightly backup at 03:00. At 21:00 a bad plugin update corrupts the order tables. Restoring last night's dump brings the store back as it was at 03:00, without the roughly 220 orders placed since (17 an hour for 13 hours).

Database dump everyWorst case lost
24 hoursA full day: about 240 orders
1 hourAbout 17 orders
15 minutesAbout 4 orders

The database is the part that changes with each order, so dump it far more often than you archive wp-content. Compare their sizes with wp db size and du -sh /var/www/shop.example.com/wp-content/uploads. For loss measured in seconds, add binary log point-in-time recovery. RPO and RTO explains how to set these targets.

Dump the database every hour

wp db export reads the credentials from wp-config.php and passes --single-transaction to mysqldump, which reads InnoDB tables from one consistent snapshot without locking them, so checkout keeps working during the dump. This script writes under a temporary name, checks that the dump finished, and keeps 48 hours of dumps. It names files in UTC so they compare directly with WooCommerce's date_created_gmt column.

/usr/local/bin/shop-db-dump.sh
#!/bin/bash
set -euo pipefail

SITE="/var/www/shop.example.com"
BACKUP_DIR="/var/backups/shop"
KEEP_MINUTES=2880
OUT="$BACKUP_DIR/shop-db-$(date -u +%Y-%m-%d_%H%M).sql.gz"

mkdir -p "$BACKUP_DIR"
trap 'rm -f "$OUT.partial"' EXIT

sudo -u www-data /usr/local/bin/wp --path="$SITE" db export --single-transaction - | gzip > "$OUT.partial"
gunzip -c "$OUT.partial" | tail -n 1 | grep -q "Dump completed"
mv "$OUT.partial" "$OUT"

find "$BACKUP_DIR" -name 'shop-db-*.sql.gz' -type f -mmin +"$KEEP_MINUTES" -delete
Terminal
chmod 755 /usr/local/bin/shop-db-dump.sh
/etc/cron.d/shop-db-dump
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

5 * * * * root flock -n /run/lock/shop-db-dump.lock /usr/local/bin/shop-db-dump.sh >> /var/log/shop-db-dump.log 2>&1

set -o pipefail makes a failed export fail the script instead of leaving a small gzip file that looks fine, and flock -n stops two runs overlapping. Keep the nightly file archive from the WordPress guide, and copy both off the server, for example with rclone.

Restore without losing new orders

First decide whether the database needs restoring at all. If an update broke the code but the data is fine, restore only the plugin or theme folder from the file backup and keep the live database. One exception: WooCommerce's update guide asks for a current backup before you run its database update, and rolling back only the plugin files after one is not a documented path.

For planned work, a move or a risky update, close the shop first so no order arrives between the final dump and the switch. WooCommerce's update guide uses Coming soon mode for this, but that setting lives in the database, so importing a dump switches it back. WordPress's maintenance mode is a .maintenance file in the site root, which a database import cannot touch. Every web request then gets a 503, while WP-CLI keeps working:

Terminal
sudo -u www-data wp --path=/var/www/shop.example.com maintenance-mode activate

WordPress ignores a .maintenance file older than 10 minutes and reopens the site. For a longer job, renew it with maintenance-mode activate --force every few minutes; without --force the command stops with Maintenance mode already activated.

If the database is damaged and orders have arrived since the last dump, keep them. WooCommerce's Subscriptions restore guide lists what a restore loses: orders, customer accounts and saved payment tokens from the gap period, plus subscription renewals that run again because their scheduled actions come back as pending. With maintenance mode on, save the current database as it is, even if it is broken:

Terminal
sudo -u www-data wp --path=/var/www/shop.example.com db export --single-transaction - | gzip > /var/backups/shop/pre-restore-$(date -u +%Y-%m-%d_%H%M).sql.gz

List the orders placed after the dump you will restore; use the time in its file name. On an older store, query wp_posts for post type shop_order and post_date_gmt instead:

Terminal
sudo -u www-data wp --path=/var/www/shop.example.com db query "SELECT id, status, total_amount, billing_email, payment_method, transaction_id, date_created_gmt FROM wp_wc_orders WHERE type = 'shop_order' AND date_created_gmt > '2026-10-04 03:05:00' ORDER BY id" > /root/orders-since-backup.tsv

Piped to a file, the output is tab-separated rather than an ASCII table. If you are not sure which dump is good, restore it on a staging server first, set up as in the next section, while the live store stays closed. Then import the backup:

Terminal
gunzip -c /var/backups/shop/shop-db-2026-10-04_0305.sql.gz | sudo -u www-data wp --path=/var/www/shop.example.com db import -

Before reopening, look for subscription renewals that already ran on the broken store but are pending again. Delete those with wp action-scheduler action delete <id> and fix each subscription's next payment date, as WooCommerce's restore guide describes:

Terminal
sudo -u www-data wp --path=/var/www/shop.example.com action-scheduler action list --hook=woocommerce_scheduled_subscription_payment --status=pending --per_page=100

Recreate the listed orders in WooCommerce > Orders > Add order, or with wp wc shop_order create, using the saved database and your payment provider's records, then run maintenance-mode deactivate.

Keep staging copies from charging cards

A restored copy carries live gateway keys, live webhooks and real customer addresses. WooCommerce Subscriptions protects itself only when the URL changes: it records the URL where it was first activated, and on a different URL it stops automatic payments and subscription emails. Restored under the same domain, for example on a new server before DNS moves, it charges renewals as normal. WooCommerce's fix is to change the site URL or disable Action Scheduler's default queue runner, using the free Action Scheduler – Disable Default Runner plugin. On any copy that is not live:

  1. Add define( 'WP_ENVIRONMENT_TYPE', 'staging' ); to wp-config.php. WooPayments then only allows test accounts, and other plugins can read it with wp_get_environment_type().
  2. List the gateways with wp wc payment_gateway list --fields=id,enabled --user=admin, then turn off each live one: wp wc payment_gateway update stripe --enabled=false --user=admin.
  3. List webhooks with wp wc webhook list --fields=id,name,status,delivery_url --user=admin and pause each: wp wc webhook update 3 --status=paused --user=admin.
  4. Stop all email with the must-use plugin below. Must-use plugins load automatically and cannot be switched off in wp-admin.
wp-content/mu-plugins/staging-no-mail.php
<?php
// Staging only: WordPress sends no email. Never copy this file to the live site.
add_filter( 'pre_wp_mail', '__return_false' );

Prefix the wp commands with sudo -u www-data and --path as above. WooCommerce's CLI needs --user with an administrator's login or ID. The pre_wp_mail filter (WordPress 5.7 and later) stops wp_mail() before it sends anything.

When the live store moves to a new domain, update what points at it. The Stripe extension receives payment events at https://www.example.com/?wc-api=wc_stripe; its Configure connection screen has a Reconfigure webhooks button, so use it only on the store that should receive live payments. WooPayments ties its account to a site identity and may turn on Safe Mode when a site is cloned; its site migration guide covers both cases.

Customer data in your backups

Every dump holds names, addresses, emails, phone numbers and order histories, which is personal data under the GDPR. Encrypt the backups and set a retention period you can state in your privacy policy. WooCommerce's own retention settings (WooCommerce > Settings > Accounts & Privacy) trash old pending, failed and cancelled orders, anonymize completed ones and delete inactive accounts, but only in the live database; older backups keep the data until they expire.

The UK regulator's guidance on erasure says a backup may keep erased data until it is overwritten on schedule, if you tell the person so and use that backup for nothing else. Keep a log of erasure requests, and after any restore, run the requests made since the backup again.

Test the restore

Restore the newest dump and file archive on a spare server with the staging settings above, then compare order counts with the live store:

Terminal
sudo -u www-data wp --path=/var/www/shop.example.com db query "SELECT COUNT(*), MAX(date_created_gmt) FROM wp_wc_orders WHERE type = 'shop_order'"

The newest date is the last order you would get back, so the gap to now is the loss you are really running with. Then open a product with images, download a downloadable product, place a test order with an offline method such as Cash on delivery, and check WooCommerce > Status > Scheduled Actions for failures. Testing restores shows how to make this routine.

Common errors

ErrorFix
Error: Sorry, you cannot list resources. {"status":401}WooCommerce's CLI needs a user: add --user=admin.
Maintenance mode already activated.Renew it with wp maintenance-mode activate --force.
Orders from today are missing after a restoreThey were placed after the dump. List them from the pre-restore copy and recreate them.
A customer was charged twice for a renewalA renewal that already ran came back as pending. Delete processed actions before reopening.
Subscriptions shows a Staging badge on the live storeThe URL changed. Use Enable Automatic Payments in the notice, on the live site only.
WooPayments is stuck in test mode after pushing staging liveRun wp option delete wcpay_account_data and wp option delete wcpay_onboarding_test_mode.
A webhook switched itself to DisabledWooCommerce disables a webhook after 5 failed deliveries in a row. Fix the endpoint, then set it back to active.
Downloadable files are missingwp-content/uploads/woocommerce_uploads was not restored. On Nginx, WooCommerce's .htaccess does not protect it; follow its Nginx section.

Frequently asked questions

Does a WordPress backup cover WooCommerce?
Yes, if it includes the whole database and wp-content/uploads. Orders, customers, payment tokens and scheduled jobs are database tables; product images and downloadable files are in uploads.
How often should I back up a WooCommerce store?
Dump the database at least hourly on a store that takes orders every day, and archive the files nightly. A restore loses every order placed after the dump it uses.
Can I restore a WooCommerce backup without losing orders?
Not automatically. Put the site in maintenance mode, save the current database, list the orders placed since the backup, import the backup, then recreate those orders.
Where does WooCommerce store orders?
Stores using High-Performance Order Storage, the default for new installs since WooCommerce 8.2, keep them in wp_wc_orders and three related tables. Older stores keep them in wp_posts and wp_postmeta.
Will a staging copy charge my customers?
It can. Subscriptions only switches to staging mode when the URL changes. Set WP_ENVIRONMENT_TYPE to staging, disable live gateways, pause webhooks and block outgoing email on every copy.

How this was checked

Commands, limits and prices were checked against these official pages, on October 4, 2026: