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.
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):
| What | Where |
|---|---|
| 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 items | wp_woocommerce_order_items, wp_woocommerce_order_itemmeta |
| Order notes | wp_comments, comment type order_note |
| Customers | WordPress users (wp_users, wp_usermeta), plus wp_wc_customer_lookup for Analytics |
| Saved payment methods | wp_woocommerce_payment_tokens, wp_woocommerce_payment_tokenmeta |
| Scheduled jobs | wp_actionscheduler_actions, _logs, _groups, _claims |
| Webhooks, REST API keys, settings | wp_wc_webhooks, wp_woocommerce_api_keys, wp_options |
| Product images | wp-content/uploads |
| Downloadable product files | wp-content/uploads/woocommerce_uploads |
| Logs | wp-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:
sudo -u www-data wp --path=/var/www/shop.example.com option get woocommerce_custom_orders_table_enabledOne 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 every | Worst case lost |
|---|---|
| 24 hours | A full day: about 240 orders |
| 1 hour | About 17 orders |
| 15 minutes | About 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.
#!/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" -deletechmod 755 /usr/local/bin/shop-db-dump.shPATH=/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>&1set -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:
sudo -u www-data wp --path=/var/www/shop.example.com maintenance-mode activateWordPress 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:
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.gzList 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:
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.tsvPiped 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:
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:
sudo -u www-data wp --path=/var/www/shop.example.com action-scheduler action list --hook=woocommerce_scheduled_subscription_payment --status=pending --per_page=100Recreate 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:
- Add
define( 'WP_ENVIRONMENT_TYPE', 'staging' );to wp-config.php. WooPayments then only allows test accounts, and other plugins can read it withwp_get_environment_type(). - 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. - List webhooks with
wp wc webhook list --fields=id,name,status,delivery_url --user=adminand pause each:wp wc webhook update 3 --status=paused --user=admin. - Stop all email with the must-use plugin below. Must-use plugins load automatically and cannot be switched off in wp-admin.
<?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:
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
| Error | Fix |
|---|---|
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 restore | They were placed after the dump. List them from the pre-restore copy and recreate them. |
| A customer was charged twice for a renewal | A renewal that already ran came back as pending. Delete processed actions before reopening. |
| Subscriptions shows a Staging badge on the live store | The URL changed. Use Enable Automatic Payments in the notice, on the live site only. |
| WooPayments is stuck in test mode after pushing staging live | Run wp option delete wcpay_account_data and wp option delete wcpay_onboarding_test_mode. |
| A webhook switched itself to Disabled | WooCommerce disables a webhook after 5 failed deliveries in a row. Fix the endpoint, then set it back to active. |
| Downloadable files are missing | wp-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:
- WooCommerce developer docs: High Performance Order Storage
- WooCommerce docs: Installed Database Tables
- WooCommerce wiki: Database Description
- WooCommerce developer docs: Logging
- WooCommerce docs: Digital/downloadable product handling
- WooCommerce docs: How to update WooCommerce
- WooCommerce docs: Coming soon mode
- WooCommerce docs: Restoring backups with WooCommerce Subscriptions
- WooCommerce docs: How Subscriptions handles staging sites and migrations
- WooCommerce docs: WooPayments site migrations
- WooCommerce docs: WooPayments test accounts (dev mode)
- WooCommerce docs: Stripe extension, setting up webhooks
- WooCommerce docs: Accounts and privacy settings (personal data retention)
- WooCommerce developer docs: WC-CLI shop_order, webhook and payment_gateway commands
- WooCommerce developer docs: WC-CLI FAQ (--user)
- WooCommerce REST API docs: orders and webhooks
- WooCommerce source: OrdersTableDataStore schema and order notes
- Action Scheduler: WP-CLI commands
- WP-CLI: wp maintenance-mode activate (and its source)
- WordPress Code Reference: wp_is_maintenance_mode()
- WP-CLI: wp db query
- MySQL 8.4 Reference Manual: mysql client (tab-separated output when not interactive)
- WP-CLI: wp db size
- WordPress Code Reference: pre_wp_mail and wp_get_environment_type()
- WordPress Advanced Administration: Must Use Plugins
- ICO (UK GDPR guidance): Right to erasure, backup systems