"mysqldump: Got error: 145 … when using LOCK TABLES": one damaged MyISAM table and a week of empty backups

For seven nights the cron backup script wrote a 751 B file instead of about 280 MB: one MyISAM table was marked as crashed, mysqldump stopped at LOCK TABLES, the script exited with code 0 and cron discarded the message. We reproduced the failure, repaired the table, moved the databases to InnoDB and introduced a backup that test-restores itself every night and raises an alarm.

  • Backup file: 751 B instead of about 280 MB, script exit code 0
  • mysqldump exit code 2, message discarded by cron (No MTA installed)
  • New backup: test restore of 48 tables in 32 to 34 s, every night
A list of nine backup files with the same time 01:30. The top two rows have a long black bar and the size 285 MB, the seven below only a short orange mark and the size 751 B

Docker server with MySQL 5.7.44 and several WordPress databases, including a WooCommerce shop

Industry: Retail and commerce

Challenge

A Docker server with MySQL 5.7.44 ran several WordPress sites, including a WooCommerce shop, with tables in the old MyISAM storage engine. A bash script on the host wrote every database to its own <database>.sql file each night, with no date in the name. The client checked that the backups were being made. That went on for three years.

Then the shop database had to be restored. The backups of the last seven nights were 751 bytes each instead of 200 to 300 MB: the header, CREATE DATABASE, USE, the end. Only the backup from eight days earlier was complete. The site and the admin panel worked the whole time, and neither the script nor cron reported an error.

The task: find out why the backups were empty although everything seemed fine, remove the cause and build a backup that proves by itself that you can come back from it.

Solution

The 751 B file told us the most: the dump stopped before the first table, it had not saved an empty database. We rebuilt the client's server with the same database version and the same kind of script, and triggered the failure the way it happens in real life: the database disk filled up while the shop was running, then the container was restarted. The result: a file built exactly like the client's, down to the byte.

We repaired the damaged table and checked all the others, moved the databases to InnoDB and rewrote the backup: exit code, end-of-dump marker, size, then a test restore of every database into a separate MySQL 5.7.44 instance with a comparison of tables and rows. Only a verified backup gets a name with a date, and every error sends an e-mail. On top of that, a disaster recovery plan, rehearsed on an empty server (as of 2026-11).

Results

Shop backup with a damaged table
751 B for seven nights with exit code 0, now an e-mail alarm on the first night with exit code 1
Last good backup
14 days of history, no backup is ever overwritten
Test restore of the shop database
48 tables and 3.2 million rows in 32 to 34 s, every night
Full backup with test restore
65 s for three databases including the test instance
Repair of the damaged table
0.47 s REPAIR TABLE, 2.1 s check of all tables
Shop rebuilt on a clean server
50 s from an empty instance, the drill found 3 gaps in the backups

How we worked

  1. The 751 B file line by line

    The MariaDB client's sandbox marker, no comments, no footer, the end after USE. The dump stopped before the first table, the database was not empty.

  2. The server log

    About 70 lines of "marked as crashed" and "Checking table:" within one second after the restart: MyISAM messages and automatic table checks switched on.

  3. Rebuilding the environment

    MySQL 5.7.44 in Docker with MyISAM tables, three WordPress sites, a WooCommerce shop with 8,000 products and 32,400 orders, the same kind of cron script on the host with the MariaDB client from Debian 12.

  4. A real failure, no edited files

    A full database volume while the shop was running, then docker stop. Three runs of the same scenario, two of them left a permanently damaged table.

  5. Backup and site with a damaged table

    The old cron script, three variants of mysqldump, shop and admin pages, the guest cart through the WooCommerce Store API.

  6. Restore attempts

    The 743 B file and a full dump into a fresh MySQL 5.7.44 with the clients mysql 5.7.44, 8.0.46, 8.4.13 and mariadb 10.11, a dump from the older mariadb-dump 10.11.8, the client from Debian 13.

  7. Repair, new backup and drill

    REPAIR TABLE and a check of all tables, the move to InnoDB, the new script on the healthy and on the damaged database, the whole shop rebuilt on a clean server against the clock.

What mysqldump writes when one of 48 tables is damaged (reproduction, as of 2026-11)

Variant Exit code File Tables in the file End of file
default, --lock-tables 2 743 B 0 of 48 after USE, no footer
--force 2 210,083,890 B 47 of 48 normal footer
--single-transaction 2 210,078,388 B 42 of 48, stopped at the session table cut off halfway
default after REPAIR TABLE 0 285,934,637 B 48 of 48 normal footer

Challenges we overcame

The site and the admin panel worked, the script and cron reported no error. Below are the messages as MySQL, mysqldump, WordPress and cron wrote them. In the client's material the database name is replaced with <database>; in the reproduction the shop database is called sklep.

The exact messages

The client's backup file, 751 B, its entire content:

/*M!999999\- enable the sandbox mode */

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;

CREATE DATABASE /*!32312 IF NOT EXISTS*/ `<database>` /*!40100 DEFAULT CHARACTER SET utf8 */;

USE `<database>`;

The first line is the sandbox mode marker that only the MariaDB client writes, so the dump came from mysqldump of the mariadb-client package on the host (on Debian 12 a link to mariadb-dump), not from the tool in the MySQL container. No comments means --skip-comments, which also removes the closing -- Dump completed on … marker. No footer means the dump was interrupted: an empty database gives a file with a footer (1,173 B in the reproduction).

The client's server log after the restart, about 70 lines in the same second:

[ERROR] mysqld: Table './<database>/wp_options' is marked as crashed and should be repaired
[ERROR] mysqld: Table './<database>/wp_postmeta' is marked as crashed and should be repaired
[Warning] Checking table:   './<database>/wp_postmeta'
[ERROR] mysqld: Table './<database>/wp_woocommerce_sessions' is marked as crashed and should be repaired
[Warning] Checking table:   './<database>/wp_woocommerce_sessions'

Checking table: only appears when myisam-recover-options is switched on. In the mysql:5.7.44 image it defaults to OFF, so the client's server had it in its configuration.

Reproduction: a full disk. MyISAM does not return an error, it waits for space and holds the table lock, while the home page keeps answering:

[ERROR] Disk is full writing './sklep/wp_woocommerce_sessions.MYD' (Errcode: 15795968 - No space left on device). Waiting for someone to free space...
[ERROR] Retry in 60 secs. Message reprinted in 600 secs

Restarting the database container, the usual reaction. Docker gives the process 10 s, then SIGKILL:

$ docker stop db        # 10.5 s, ExitCode 137
[Note] Giving 7 client threads a chance to die gracefully
[Warning] mysqld: Forcing close of thread 31  user: 'sklep'
[ERROR] mysqld: Incorrect key file for table './sklep/wp_woocommerce_sessions.MYI'; try to repair it
[ERROR] Got an error from thread_id=31, .../storage/myisam/mi_write.c:232

WooCommerce's session cleanup a few hours later:

WordPress database error Incorrect key file for table './sklep/wp_woocommerce_sessions.MYI'; try to repair it for query DELETE FROM wp_woocommerce_sessions WHERE session_expiry < 1793638328 ORDER BY session_expiry LIMIT 100 made by ... wc_cleanup_session_data, WC_Session_Handler->cleanup_sessions

From then on, every time the table is opened again it ends with:

ERROR 145 (HY000): Table './sklep/wp_woocommerce_sessions' is marked as crashed and should be repaired

The backup from cron with the damaged table, output of the old script:

Kopia bazy blog
Kopia bazy firma
Kopia bazy sklep
mysqldump: Got error: 145: "Table './sklep/wp_woocommerce_sessions' is marked as crashed and should be repaired" when using LOCK TABLES
Kopia zakonczona: Mon Nov  2 16:53:55 UTC 2026
kod wyjscia skryptu: 0
CRON[107]: (root) CMD ([108] /root/skrypty/backup-mysql.sh)
CRON[107]: (CRON) info (No MTA installed, discarding output)
CRON[107]: (root) END ([108] /root/skrypty/backup-mysql.sh)

The shop file shrank from 285,757,704 B to 743 B, with the current date. With a database name as long as the client's it comes to exactly 751 B.

The damage works like a delayed fuse

After the restart everything worked and the 285 MB backup succeeded. The table was only flagged when WooCommerce's session cleanup (every 12 hours) hit the inconsistent index, and opening it only failed once the table had been reopened. Backups start failing hours or days after the incident, when nobody connects the two any more. Killing the database process alone did not damage the table for good; that took the full disk plus docker stop, and a third run of the same scenario ended with just a warning. Whether a restart breaks a table depends on the moment.

Fixing what you can see leaves the rest

In the second run wp_options failed too, and the shop showed "Error establishing a database connection". WordPress's tool wp-admin/maint/repair.php only checks the 12 core tables plus whatever plugins add through the tables_to_repair filter; WooCommerce adds nothing. The site came back, the damaged session table stayed. On top of that the tool reported "Failed to repair the wp_options table" although the table had been repaired: it only reads the first row of the REPAIR TABLE result, and status OK is in the last one. mysqlcheck --all-databases --check also printed Corrupt and exited with code 0, so as a monitoring check it would have been useless.

One table stops the whole database, and the "fixes" produce worse files

With the default --lock-tables the dump starts with a single LOCK TABLES on every table of the database, so one flagged table stops them all. A 743 B file stands out if anyone looks at the size. --force gives 210 MB missing one table and with a normal ending, --single-transaction gives 210 MB cut off halfway through the table list. Both look like a backup. The new script's partial file was 82 % of the previous backup's size, so it would have passed the common "alarm below 80 %" threshold.

The file date proves nothing, and a name without a date destroys the last good backup

The shell truncates the file at > <database>.sql before mysqldump even connects. The file gets a fresh date every night, and the first failed night replaces the last good backup. The complete version from eight days earlier had to come from another backup of the server.

The script and cron report success

mysqldump returns 2, the script 0, because the last command is an echo. The message goes to stderr, cron tries to mail it, the host has no mail server, so cron discards it and only logs END, without an exit code.

The site and the admin panel show nothing

With the session table damaged, the home page, shop, product page, search, login and every admin page we checked answered 200 without a message. There was one symptom: the guest cart held 0 items after adding a product. The only trace in the admin panel is WooCommerce, Status, Database, which calls the damaged table missing: "Missing base tables: wp_woocommerce_sessions".

Restoring an empty backup "succeeds", and backups can stop working without any failure

Loading the 743 B file ends with exit code 0 and an empty database in every client we tried: mysql 5.7.44, 8.0.46, 8.4.13 and mariadb 10.11. The sandbox marker in the client's file (/*M!) does not bother MySQL clients, but dumps from the older mariadb-dump 10.11.8 start with /*! and load nothing:

ERROR at line 1: Unknown command '\-'.

And after the host was upgraded to Debian 13 (mariadb-client 11.8.6) the same script already fails on the list of databases, because newer MariaDB clients verify the server certificate by default and MySQL 5.7 has a self-signed one. Zero files, exit code 0, "Kopia zakonczona":

ERROR 2026 (HY000): TLS/SSL error: Certificate verification failure: The certificate is NOT trusted.
A chain of five links joined by orange connectors: a disk labelled 100 %, an index file labelled .MYI with a crack, a flag labelled crashed, a padlock labelled LOCK TABLES 145 and a short file labelled 751 B and exit 0

The repair, and a backup that checks whether you can come back from it

Repair and a check of all tables

mysqlcheck -h db -uroot -p --all-databases --check --silent     # 2.1 s, prints only problems
REPAIR TABLE sklep.wp_woocommerce_sessions;
-- info     Found block that points outside data file at 70053148
-- warning  Number of rows changed from 74527 to 74695
-- status   OK                                          (0.47 s)
CHECK TABLE sklep.wp_woocommerce_sessions;               -- status OK

After the repair the old script wrote 285,934,637 B again, and the guest cart held 1 item. You have to read the output of mysqlcheck, not its exit code.

InnoDB instead of MyISAM

After a hard shutdown InnoDB recovers a consistent state from its log, and it allows a consistent dump without table locks (--single-transaction). The same full-disk scenario after the move: InnoDB does not wait, it rejects the write straight away.

[ERROR] InnoDB: posix_fallocate(): Failed to preallocate data for file ./sklep/wp_woocommerce_sessions.ibd, desired size 4194304 bytes. Operating system error number 28. Check that the disk is not full or a disk quota exceeded. ...
[ERROR] mysqld: The table 'wp_woocommerce_sessions' is full

While the disk was full, 2,752 of 4,777 requests (58 %) ended with code 500. In exchange docker stop took 4.1 s and ended cleanly, and once space was freed mysqlcheck found no problem. A visible error for a few minutes instead of an invisible one for a week.

The trap in the move: MySQL 5.7's default sql_mode contains NO_ZERO_DATE, NO_ZERO_IN_DATE and STRICT_TRANS_TABLES, while WordPress columns have DEFAULT '0000-00-00 00:00:00'. A plain ALTER TABLE failed on 19 of 72 tables:

ERROR 1067 (42000) at line 1: Invalid default value for 'post_date'

WordPress sets a more lenient mode in its own session, which is why nobody notices day to day. The move runs in a maintenance window, because ALTER TABLE copies the table and blocks writes:

SET SESSION sql_mode = 'NO_ENGINE_SUBSTITUTION';
ALTER TABLE sklep.wp_posts ENGINE=InnoDB;   -- and the same for every MyISAM table
DatabaseTime to convertData and indexes before and after
sklep, 48 tablesabout 95 s393.5 MB and 627.9 MB
blog3.6 s11.3 MB and 15.4 MB
firma2.7 s0.5 MB and 1.2 MB

In the configuration default-storage-engine goes back to InnoDB so that new plugin tables are not created as MyISAM, and we size innodb_buffer_pool_size to the server's memory, because after the move the shop database is larger than the default 128 MB.

The new backup: the key parts of the script

set -uo pipefail
# dump with the tool from the database container: same version as the server, no surprises from the MariaDB client
docker exec -e MYSQL_PWD="$DB_PASS" "$DB_CONTAINER" mysqldump -uroot --single-transaction --quick \
    --routines --triggers --events --default-character-set=utf8mb4 --databases "$db" 2> "$dir/.stderr" | gzip -6 > "$part"
rc=("${PIPESTATUS[@]}")
[ "${rc[0]}" = 0 ] && [ "${rc[1]}" = 0 ] || fail "$db" "mysqldump kod ${rc[0]}, gzip kod ${rc[1]}"
# completeness: mysqldump writes this marker only after a successful end (hence no --skip-comments)
zcat "$part" | tail -n 1 | grep -q '^-- Dump completed' || fail "$db" "brak znacznika '-- Dump completed'"
# size compared with the previous successful backup
[ $(( size * 100 / psize )) -ge 80 ] || fail "$db" "kopia ma $ratio% rozmiaru poprzedniej"
# test restore into a fresh mysql:5.7.44 instance and comparison with production
zcat "$part" | docker exec -i -e MYSQL_PWD=test "$TEST_CONTAINER" mysql -uroot || fail "$db" "przywrócenie nie powiodło się"
#   same table list, rows: tables > 1000 rows at least 90%, smaller ±5, wp_options/siteurl readable
# only now the target name with a date; earlier backups are never overwritten
mv "$part" "$dir/$db-$STAMP.sql.gz"

Alongside it: an export of accounts and privileges (SHOW CREATE USER, SHOW GRANTS), because the mysql database is not part of the application dumps; 14 days of rotation; an e-mail over SMTP without a local mail server and exit code 1 on every error; after success a signal to an external monitor that raises an alarm when the backup does not run at all.

DatabaseDump.sql.gz fileTest restore
sklep4 to 5 s33.4 MB48 tables, 3,175,139 rows, 32 to 34 s
blogup to 1 s1.9 MB12 tables, 12,067 rows, 2 s
firmaunder 1 s88 kB12 tables, 361 rows, 1 s

The whole run for three databases, including starting the test instance: 65 s. The same script on a copy of the database with the damaged MyISAM table (the script's messages are in Polish):

=== sklep 18:26:33
UWAGA: sklep ma 48 tabel innych niz InnoDB (zrzut bez spojnosci transakcyjnej dla nich)
mysqldump: Couldn't execute 'show create table `wp_woocommerce_sessions`': Table './sklep/wp_woocommerce_sessions' is marked as crashed and should be repaired (145)
BLAD [sklep]: mysqldump kod 2, gzip kod 0: ...
KONIEC Z BLEDAMI: 1

The e-mail with the error message arrived within the same minute. The two previous shop backups stayed untouched, and the partial file was kept as .part for diagnosis.

A lane from a database through four gates with ticks labelled exit 0, Dump completed, 80 % and restore, the restore gate in orange, a dated file at the end; a line drops from the gates to an alarm envelope

The disaster recovery plan and a test of the whole shop

The plan fits on one page and is kept off the server:

  • Goals: how much data may be lost (up to 24 hours with a nightly backup) and how long the shop may be down.
  • What the backup contains: databases, database accounts and privileges, site files (wp-content, wp-config.php), Compose files and service configuration, image versions. A database backup is not a system backup.
  • Where: at least one copy off the server and outside the account that could delete it.
  • A step-by-step procedure, with passwords in a separate, known place, and a person in charge.
  • Test: an automatic test restore of the databases every night, and once a quarter a rebuild of everything on a clean machine, timed.

The full test: an empty MySQL 5.7.44 instance, the latest backups of the three databases, the accounts, the shop's file archive, a fresh WordPress container.

StepTime from start
empty MySQL 5.7.44 ready12.7 s
shop database imported (31.7 MB gz)44.8 s
firma and blog databases imported, accounts and privileges47.9 s
shop files unpacked (204 MB)48.7 s
shop answers 200, Store API returns 8,000 products50.2 s

What the drill revealed, each effect checked separately:

  • Without the database accounts the shop does not start: "Error establishing a database connection", HTTP 500. Dumps made with --databases contain no accounts from the mysql database, hence the separate account export in the new script.
  • Under a new address WordPress redirects (301) to the address in siteurl. The test needs the production Host header or an entry in hosts.
  • Plugins that Compose mounts from outside the site directory were missing from the file archive. The restored shop ran, but without two must-use plugins carrying shop logic, and nothing reported an error. The file backup has to cover every path under volumes: in the Compose file.
Five bars on a timeline from 0 to 50 s, one per step of the rebuild: mysqld up to 12.7 s, import up to 44.8 s, GRANT up to 47.9 s, wp-content up to 48.7 s and an orange bar HTTP 200 up to 50.2 s

What we take from it

  • A backup nobody has restored from is just a file. The file was written every night for three years, and its content stopped being right without any signal.
  • Exit code, end-of-dump marker and size are the minimum, but they are not enough. A partial dump would have passed the size threshold. What settles it is loading the backup automatically into a separate database and comparing it with production.
  • A failure breaks backups with a delay. After every incident with the disk or the power supply we check all tables, not only the ones that broke the site.
  • A recovery plan only means something after the first drill on a clean machine. That is what shows what the backups are missing. Findings like these are typical when we take over and maintain a system.

Work on the shop database itself, from the query plan to the indexes, is described in the SQL indexes project, and a change prepared and carried out on a production server in the ColdFusion 2025 project.

Not sure whether your database backup can be restored? We will check it with a test restore on a separate system, without touching the production database, and show you what the backups are missing. The first step is an intro call.

Sources

We checked versions and defaults in the reproduction; the description of the options comes from the vendors' documentation (as of 2026-11):

Before and after. On the left a short file labelled 751 B and exit 0 and a crossed-out envelope. On the right a dated file, a tick labelled restore 48/48 and an orange alarm envelope

The first step: a database check with a restore test

Database check: MySQL and MariaDB

For companies whose application depends on a MySQL or MariaDB database and who want to know what happens in a failure and whether yesterday's data can really be restored.

  • Backups: what is backed up, how often and where the copies are kept
  • A restore test on a separate system, with the time it takes measured
  • Replication and resilience: what exists and what is missing
  • The slowest queries and missing indexes
  • Version, support status and configuration
  • Recommendation: backup, replication or cluster, depending on your needs

A cluster such as MariaDB Galera protects against the failure of a server, not against deleted data. That takes tested backups, which is why we start with the restore test. We agree with you on the system it runs on. We change nothing on your production database.

What the result contains

A written report with the result of the restore test, the risks in order of priority and a recommendation with the effort involved. The report is yours. If you wish, we then set up the solution, including a Galera cluster.

from €1,900 excl. VAT

Typically 2 working days

Request a database check

Related situations

  • Your system runs, but nobody knows it in detail. We record application, database, server and email, assess the risks, recommend the next step in writing and, if you wish, take over operations.

  • We move your Lucee, ColdFusion or PHP application from classic hosting into containers without rewriting it. That lets you rebuild, update and relocate the environment. Security updates for PHP 8.2 end on 31 December 2026 (As of 2026-10).

  • Support for Debian 11 ended on 31 August 2026, and for CentOS 7 on 30 June 2024 (As of 2026-10). We will upgrade your server, or plan and carry out the move to Debian. We also work with Proxmox VE 8 and with physical servers that we move to Proxmox.

More projects

  • Frame Connector/J 5.1.44 with two connections: to the server 5.5.5-10.11.19-MariaDB straight with a tick, to the server 11.8.8-MariaDB stopped at an orange padlock TLSv1.1 with a cross

    ColdFusion after a MariaDB upgrade: Connector/J 5.1 and the move to MariaDB Connector/J

    After MariaDB moved from the 10.11 branch to a newer one, ColdFusion with a manually added MySQL Connector/J 5.1.44 stopped connecting: every query failed after 4 s with "Could not create connection to database server". The cause was the server's new version string, which made the driver switch on TLS in a version Java 21 blocks. One script moved every data source to MariaDB Connector/J.

  • Two panels: WordPress + Elementor and Craft CMS in an orange frame, each with three bars for JS, SQL and PHP files; the Craft bars are shorter, the JS bar almost disappears

    WordPress vs Craft CMS: the same three websites built in both systems and measured

    We had rebuilt three company websites on WordPress with the free Elementor, then built them again from scratch in Craft CMS 5: same content, same WebP files, same look 1:1, same server. Craft sends 5 KB of JavaScript instead of 97 to 129 KB and runs fewer than half the SQL queries at a similar TTFB. As typical WordPress sites with 9 to 21 plugins, they had scored 30 to 72 in Lighthouse mobile.

In 20 minutes we work out which first step fits your system

You tell us the goal and the current state, and we say openly whether and how we can help. Reply within one working day, no obligation.

Phone (German, Polish, English) +48 501 250 220
Email Email

Reachable: Monday to Friday, 9:00 to 17:00 Central European Time; support and emergencies around the clock (24/7)