Test Your MySQL 8.4 Upgrade With Real App Queries
Install proxymock with your coding agent
Before you start, paste this into Claude Code, Cursor, Codex, Gemini CLI, Kiro, or any assistant that can read a URL and run commands:
Install proxymock for me on this machine. First fetch the agent skill at
https://raw.githubusercontent.com/speedscale/skills/main/skills/install-speedscale/SKILL.md
and the references/ and scripts/ files it links to (same base URL), save them
under your skills directory, then follow the skill in local mode: proxymock and
its MCP server only, no Kubernetes operator.
The install-speedscale skill installs proxymock for your operating system and walks you through proxymock init. It stops when you need to complete browser sign-in, keeps your recordings on your machine, and connects the proxymock MCP server so your assistant can run the prompts later in this article.
On April 21, 2026, MySQL 8.0 moved into Oracle’s Sustaining Support, which means no more updates. If your app still runs on it, the move to 8.4 LTS is on someone’s list, and the question is always the same: what breaks?
The changes from MySQL 8.0 to 8.4 give a long answer. mysql_native_password is no longer enabled by default. In the 8.4 keyword list, MANUAL and PARALLEL are new keywords, while QUALIFY and TABLESAMPLE are new reserved words. FLUSH HOSTS and the old MASTER and SLAVE statements are gone. Several InnoDB defaults changed, including the adaptive hash index, change buffering and innodb_io_capacity, which can change how the same queries perform.
None of that tells you whether your app is affected. The only list that answers that question is the list of statements your app actually sends. proxymock records that list, and it can now replay it against a real MySQL database, once to find regressions or with many sessions to load test it.
I use one recording for three jobs, with either an AI coding assistant or the equivalent proxymock command:
- A quick mock check: record, run the app against a mocked database, and replay the app’s own traffic.
- An upgrade or migration regression test: replay the recorded statements once against the new version.
- A load test: replay the recorded statements with many concurrent sessions.
flowchart LR
A[Your app] -->|real statements| R[proxymock recording]
R --> M[Mock check<br/>no database needed]
R --> G[Regression test<br/>MySQL 8.4 or a new schema]
R --> L[Load test<br/>many sessions]
The examples use the go-mysql app from the speedscale/demo repository: a small users-and-orders API that uses server-side prepared statements and a checkout transaction.
Run the copy/paste MySQL demo
You need Docker, Go 1.25 or newer, Git, curl and proxymock installed and initialized once. The commands below create a disposable MySQL 8.4 container and use the demo’s local-only demo password, so there are no credentials or paths to fill in. Do not reuse these credentials outside this local demo.
Start in a new terminal:
DEMO_DIR="$(mktemp -d /tmp/proxymock-mysql-demo.XXXXXX)"
git clone https://github.com/speedscale/demo.git "$DEMO_DIR"
printf '%s\n' "$DEMO_DIR" > /tmp/proxymock-mysql-demo.path
cd "$DEMO_DIR/go-mysql"
docker rm -f proxymock-mysql-howto >/dev/null 2>&1 || true
docker run --name proxymock-mysql-howto \
-e MYSQL_ROOT_PASSWORD=root \
-e MYSQL_DATABASE=app \
-e MYSQL_USER=demo \
-e MYSQL_PASSWORD=demo \
-p 3306:3306 \
-d mysql:8.4
until docker exec proxymock-mysql-howto mysqladmin ping \
-h 127.0.0.1 -udemo -pdemo --silent >/dev/null 2>&1; do sleep 1; done
The setup saves the checkout path in /tmp/proxymock-mysql-demo.path, so every new terminal can find the same directory without a placeholder.
1. Record the app and its real statements
In terminal 1, start proxymock. Port 13306 is the MySQL proxy; port 4143 is where you send HTTP traffic for the app that normally listens on 8080.
cd "$(cat /tmp/proxymock-mysql-demo.path)/go-mysql"
proxymock record --map 13306=localhost:3306 --app-port 8080
In terminal 2, start the demo app through the MySQL proxy:
cd "$(cat /tmp/proxymock-mysql-demo.path)/go-mysql"
MYSQL_PORT=13306 go run .
In terminal 3, copy and paste this representative session:
cd "$(cat /tmp/proxymock-mysql-demo.path)/go-mysql"
curl http://localhost:4143/health
curl http://localhost:4143/users
curl http://localhost:4143/users/1
curl -X POST http://localhost:4143/users \
-H 'Content-Type: application/json' \
-d '{"first_name":"Test","last_name":"User","email":"howto@example.com","username":"howtouser","age":40}'
curl -X PUT http://localhost:4143/users/2 \
-H 'Content-Type: application/json' \
-d '{"first_name":"Jane","last_name":"Smith","email":"jane.smith@example.com","username":"janesmith","age":29}'
curl -X POST http://localhost:4143/users/1/orders \
-H 'Content-Type: application/json' \
-d '{"total":12.50}'
curl http://localhost:4143/users/1/orders
curl -X DELETE http://localhost:4143/users/5
Stop the recorder and app with Ctrl+C after the requests finish. The new proxymock/recorded-* directory contains both sides of the session: the eight inbound HTTP calls and the outbound MySQL exchanges they caused. Prepared statements are recorded as their prepare, execute and close operations, and each execute carries its SQL and parameters.
Prefer an AI assistant? Copy this prompt into one with the proxymock MCP server installed:
Use proxymock to record my application's inbound HTTP traffic and outbound MySQL
traffic. I am in the speedscale/demo go-mysql directory. MySQL 8.4 is running at
localhost:3306 with database app, user demo and password demo. The app listens on
port 8080. Map local port 13306 to MySQL, start the recorder, and start the app
with MYSQL_PORT=13306. When the app is ready, send these requests through
http://localhost:4143: GET /health; GET /users; GET /users/1; POST /users with a
complete unique user; PUT /users/2 with a complete valid user; POST
/users/1/orders with total 12.50; GET /users/1/orders; and DELETE /users/5. Wait
for every request, stop the recorder and app cleanly, then report the recording
directory plus the HTTP and MySQL RRPair counts.
2. Prove the app works without MySQL
This is the “make sure the plumbing works” path. Stop MySQL, run the app against the mock, and replay the app’s inbound traffic at it.
In terminal 1, stop the disposable database, then start the mock and app together:
cd "$(cat /tmp/proxymock-mysql-demo.path)/go-mysql"
docker stop proxymock-mysql-howto
proxymock mock --map 13306=localhost:3306 -- env MYSQL_PORT=13306 go run .
In terminal 2, replay the recorded HTTP traffic:
cd "$(cat /tmp/proxymock-mysql-demo.path)/go-mysql"
proxymock replay --test-against http://localhost:8080
The terminal should report eight matching HTTP responses and zero failures. The app just worked end to end with MySQL stopped. This is the setup from MySQL Mocking with proxymock, used here as a starting point rather than the destination.
AI assistant prompt:
Using the newest local proxymock/recorded-* recording, stop the Docker container
named proxymock-mysql-howto. From the speedscale/demo go-mysql directory, run
`proxymock mock --map 13306=localhost:3306 -- env MYSQL_PORT=13306 go run .` and
keep that process alive. Replay the recorded inbound HTTP traffic against
http://localhost:8080, wait for the replay to finish, and report the response
match count and failures. Do not return while the replay is still running.
Flip the recording around
By default, a replay sends inbound traffic to your app as tests and serves outbound traffic, including every MySQL statement, as mocks. To test the database, the MySQL side switches jobs: the recorded statements become the tests, and the database becomes the system under test.
A tests filter does that. It chooses which recorded traffic runs as tests, and everything else is mocked. The recording itself never changes; only its role in this replay does.
flowchart TD
subgraph D[Default replay]
D1[Inbound HTTP] -->|test| D2[Your app]
D2 -->|mocked| D3[MySQL]
end
subgraph F[With a tests filter]
F1[Recorded statements] -->|test| F2[MySQL 8.4]
end
- CLI:
--tests-filter '(direction IS OUT) AND (tech IS MySQL)' - AI assistant: ask for the MySQL traffic to be replayed against the database, or pass the
tests-filterparameter of thereplay_traffictool. - proxymock web: the Tests filter field on the Replay tab.
- Speedscale dashboard: Choose replay tests in a snapshot’s actions menu, then check the database under Outbound dependencies.
The replay opens its own MySQL sessions and reads credentials where the mysql client does: MYSQL_USER and MYSQL_PWD, or the [client] section of ~/.my.cnf. It rejects credentials inside the mysql:// address, which keeps passwords out of shell history and reports. It uses TLS when the server offers it, as the mysql client does, and ?ssl-mode=VERIFY_IDENTITY on the address checks the certificate too. Before load starts, it connects once to each database, so a bad password stops the run with MySQL’s 1045 instead of failing every statement.
3. Regression test the 8.4 upgrade
Stop the mock and app with Ctrl+C. Restart MySQL and reset the disposable target database so the recording begins from the same empty state as the original run:
docker start proxymock-mysql-howto
until docker exec proxymock-mysql-howto mysqladmin ping \
-h 127.0.0.1 -udemo -pdemo --silent >/dev/null 2>&1; do sleep 1; done
docker exec proxymock-mysql-howto mysql -uroot -proot -e \
"DROP DATABASE IF EXISTS app; CREATE DATABASE app; GRANT ALL PRIVILEGES ON app.* TO 'demo'@'%';"
Now replay the recorded MySQL traffic once:
cd "$(cat /tmp/proxymock-mysql-demo.path)/go-mysql"
MYSQL_USER=demo MYSQL_PWD=demo proxymock replay \
--tests-filter '(direction IS OUT) AND (tech IS MySQL)' \
--test-against mysql://localhost:3306/app \
--fail-if 'requests.result-match-pct < 100'
The clean run should show that every result matched and exit with code 0.
Now reset the rows, rename the order_count column that the app still reads and increments, and run the exact same test again:
docker exec proxymock-mysql-howto mysql -uroot -proot app -e \
"SET FOREIGN_KEY_CHECKS=0; TRUNCATE TABLE orders; TRUNCATE TABLE users; SET FOREIGN_KEY_CHECKS=1; ALTER TABLE users RENAME COLUMN order_count TO order_count_renamed;"
MYSQL_USER=demo MYSQL_PWD=demo proxymock replay \
--tests-filter '(direction IS OUT) AND (tech IS MySQL)' \
--test-against mysql://localhost:3306/app \
--fail-if 'requests.result-match-pct < 100'
This time, statements that use order_count appear under RESULT MISMATCH. Each replayed statement keeps MySQL’s error, here 1054 Unknown column 'order_count' in 'field list', and the command exits with code 1. That failure is the successful test: it caught the incompatible migration. An unquoted column named qualify or a leftover FLUSH HOSTS in a maintenance job would show up the same way, by statement, before the cutover instead of after. A replay user still on mysql_native_password stops the run at its first connection with the server’s reason. That is a sharper signal than watching API tests pass while the database quietly changes underneath them.
AI assistant prompt:
Run the full MySQL regression demo with the newest local proxymock/recorded-*
input. Start the Docker container named proxymock-mysql-howto and wait for its
mysqladmin health check. Drop and recreate database app with docker exec. Replay
only `(direction IS OUT) AND (tech IS MySQL)` against
mysql://localhost:3306/app once with MYSQL_USER=demo and MYSQL_PWD=demo, failing
if `requests.result-match-pct < 100`. Wait for the clean replay and report its
verdict. Then use docker exec to truncate orders and users, rename
users.order_count to order_count_renamed, and run the same replay again. Wait for
it to finish, then report its exit code and list every mismatched statement with
its MySQL error number and message. Do not return while either replay is running.
4. Load test MySQL
Reset the disposable database, then run the same statements with 10 sessions for one minute:
docker exec proxymock-mysql-howto mysql -uroot -proot -e \
"DROP DATABASE IF EXISTS app; CREATE DATABASE app; GRANT ALL PRIVILEGES ON app.* TO 'demo'@'%';"
MYSQL_USER=demo MYSQL_PWD=demo proxymock replay \
--tests-filter '(direction IS OUT) AND (tech IS MySQL)' \
--test-against mysql://localhost:3306/app \
--vus 10 --for 1m --load-test
Each virtual user keeps one MySQL session for the whole run and replays the statements in order, so prepared statements and transactions carry over as they did in the app. If a pass through the recording ends inside a transaction, it is rolled back before the next pass starts.
In my validated run, 10 virtual users sent 783,143 MySQL operations in one minute, about 13,052 per second, with zero send failures. Your laptop will produce different numbers. The interesting part was the two errors that did come back.
The first was 1062 Duplicate entry on the user INSERT: every virtual user replays the same recorded usernames, so after the first one, the rest collide. That is an artifact of replaying, and the new mysql_param extractor fixes it. A two-rule blueprint regenerates the email and username of each insert with rand_string, and the duplicates went to zero, with each pass writing new users.
The second was 1213 Deadlock found when trying to get lock on the checkout’s UPDATE. That one is real. The checkout inserts an order, which takes a lock on the user row through the foreign key, and then updates the same row. Five concurrent checkouts for one user are enough for MySQL to pick a victim and roll it back. The fix belongs in the app: retry the transaction or lock the row first. A synthetic benchmark would never have found it, because it did not know this app has a checkout.
AI assistant prompt:
Run the MySQL load demo with the newest local proxymock/recorded-* input. Start
the Docker container named proxymock-mysql-howto if needed and wait for its
mysqladmin health check. Drop and recreate database app with docker exec. Use
the proxymock MCP replay_traffic tool against mysql://localhost:3306/app with
user demo and password demo. Set tests-filter to
`(direction IS OUT) AND (tech IS MySQL)`, vus to 10, for to `1m`, and load-test
to true. Keep the MCP session alive and poll list_running until the replay has
finished. Then read the completed process logs and summarize total operations,
throughput, p50/p95/p99 latency, send failures, MySQL errors and results by SQL
statement. Do not return while the replay is still running.
For a one-minute load run, use the CLI if your chatbot does not keep MCP processes alive between turns. A chatbot that exits after saying the run started can also stop the replay early.
5. Review the saved results in proxymock web
Run the web viewer from the demo directory, where the proxymock workspace lives:
cd "$(cat /tmp/proxymock-mysql-demo.path)/go-mysql"
proxymock web --port 7788 --open=false --forwarder-addr ''
Open http://127.0.0.1:7788 and use the RUN dropdown in the upper-left corner:
- Choose the
recorded-*run to show the captured HTTP and MySQL traffic. - Choose the first database
results/replayed-*run to show the clean regression. - Choose the next database
results/replayed-*run to show the broken schema. - Stay in Grid, select a row whose MATCH value is no match, and open Response. You will see MySQL error
1054andUnknown column 'order_count' in 'field list'. - Use the load-test terminal summary for aggregate throughput and latency. Load mode samples the saved rows, so the Grid does not contain every execution from a long run.
From the Speedscale dashboard
For traffic recorded in Kubernetes, the same test runs from the dashboard. Open the snapshot, choose Choose replay tests, check the database under Outbound dependencies, and save. Speedscale reanalyzes the snapshot, and the database becomes a service you can replay against. Put its credentials in the test config’s generator.mysql section, with the password stored as a secret reference.
Things to know
- Use a disposable database. Replays run real inserts, updates and deletes.
- Start from the recorded state. Recorded statements use the ids the original database handed out, so a regression test works best against a copy restored from the dump you recorded against.
- Transactions are replayed statement by statement. Recorded traffic mixes many app connections, so a replay can send a
COMMITwithout itsSTART TRANSACTION. proxymock logs a warning when that happens. - Not replayed yet:
LOAD DATA LOCAL, cursor fetches and statements prepared before recording started.
An upgrade checklist tells you what changed in MySQL. A replay of your own statements tells you which of those changes you will actually hit, and how the new version holds up under your real workload. One recording answers both.
The MySQL Load and Regression Testing guide covers the full setup, including TLS, test config credentials and the blueprint that regenerates unique values. The PostgreSQL version does the same for Postgres.