MySQL is an RDBMS with two pieces: a server (background process on port 3306) and a client (shell or GUI)
Install via apt (Linux), Homebrew (macOS), or winget (Windows) — then run mysql_secure_installation immediately
MySQL 8.4 is the current LTS release as of 2026 — avoid 8.0 for new projects, it reaches EOL in April 2026
Always create a dedicated user per application with least-privilege permissions — never use root for app connections
INSERT/UPDATE/DELETE require connection.commit() in Python — uncommitted changes silently vanish on connection close
Use DECIMAL(8,2) for money columns — FLOAT causes rounding errors like $12.98999977
The mysql_secure_installation script is not optional — skipping it leaves your database open to the internet
Plain-English First
Think of MySQL as a super-organised filing cabinet for your app. Instead of stuffing papers randomly into drawers, MySQL lets you store data in neat, labelled folders (tables) that you can search, sort, and update in milliseconds. Installing MySQL is like plugging in that filing cabinet — until you do, your app has nowhere to put its data. Once it's running, it works silently in the background, always ready to fetch exactly the file you ask for. The 2026 release of MySQL 8.4 LTS makes that cabinet sturdier and faster than ever, with authentication defaults that are actually secure out of the box — though you still need to do your part.
Every app you love — Instagram, Airbnb, Shopify — needs a place to remember things. User accounts, orders, messages, prices — all of it has to live somewhere when the server restarts. That 'somewhere' is a database, and MySQL is the most widely deployed open-source relational database on the planet. It powers roughly 40% of all websites and has been doing so reliably since 1995. If you're building anything on the web, chances are MySQL (or its cousin MariaDB) is under the hood.
The problem most beginners hit isn't the SQL language itself — it's getting MySQL installed, secured, and connected without breaking anything. A missed configuration step can leave your database wide open to the internet, locked behind a password you forgot, or refusing connections from your own app. Those errors are cryptic, confidence-shattering, and almost always avoidable.
One thing worth noting before we dive in: MySQL 8.0 reaches end-of-life in April 2026. If you're starting a new project today, install MySQL 8.4 LTS — it's the current long-term support release, supported through 2032, and it ships with caching_sha2_password as the default authentication plugin. That's actually a good thing for security, but it means a few connection steps look slightly different from older tutorials you may have found. Every command below is written for 8.4.
By the end of this article you'll have MySQL installed on your machine (Windows, Mac, or Linux), the root account secured, a real database and user created, and your first table populated with data you can query. No prior database knowledge needed. Every command is explained before you type it.
What MySQL Actually Is — And Why You Need a Server Running Locally
MySQL is a Relational Database Management System (RDBMS). 'Relational' just means data is stored in tables — rows and columns, like a spreadsheet — and those tables can be linked together. 'Management System' means there's a program that handles reading, writing, and protecting that data for you.
Here's the part beginners often miss: MySQL has two pieces, not one. There's the server — a background process (mysqld) that runs continuously, owns the data files on disk, and listens for connections on port 3306. Then there's the client — a command-line tool (or a GUI like MySQL Workbench) that you use to talk to that server. When you type a query, the client sends it over a network socket to the server, the server executes it against the data files, and sends the results back to your screen.
Why does this matter? Because 'MySQL is not running' and 'MySQL is not installed' are two completely different problems with two different fixes. The server must be started before any client can connect. On most systems, installation configures it to start automatically on boot — but that's worth verifying, and we will. It also matters when you're debugging connection errors from application code: the first thing to rule out is whether the server process is actually up, before you start questioning your connection string or credentials.
In 2026, MySQL 8.4 LTS is the version you want. MySQL 8.0 hits end-of-life in April 2026, and MySQL 9.x is the innovation track (new features, shorter support window). For anything you're building that needs to run reliably for the next few years, 8.4 is the stable choice.
check_mysql_status.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# ── Check whether the MySQL server process is running ──────────────────────
# OnLinux (systemd-based — Ubuntu, Debian, RHEL, Fedora):
sudo systemctl status mysql
# On some distributions the service name is 'mysqld' instead of 'mysql':
sudo systemctl status mysqld
# On older Linux systems still using 'service':
sudo service mysql status
# OnmacOS (installed via Homebrew):
brew services list | grep mysql
# OnWindows (PowerShell, run as Administrator):
Get-Service -Name'MySQL*'
# ── Confirm what port MySQL is listening on ────────────────────────────────
# Linux/macOS — ss replaces the older netstat on modern systems:
sudo ss -tlnp | grep 3306
# macOS alternative:
sudo lsof -i :3306
# Windows (PowerShell):
netstat -ano | findstr :3306
# What you're looking for:
# Linux/Mac: a line showing '0.0.0.0:3306' or '127.0.0.1:3306' with mysqld in the process column
# Windows: a line showing '0.0.0.0:3306' with state LISTENING
# If nothing appears on port 3306, the server is not running or is using a non-default port.
MySQL listens on TCP port 3306 by default. If a firewall, security group, or OS-level rule blocks this port, your application cannot reach the database even if MySQL is running perfectly — and the error you get ('Connection refused' or a timeout) looks identical to 'MySQL is not running'. When debugging any connection error, always confirm two things independently: (1) the server process is up, and (2) port 3306 is reachable from where you're connecting. On Linux use sudo ss -tlnp | grep 3306, on macOS use sudo lsof -i :3306, and on Windows use netstat -ano | findstr :3306.
Production Insight
MySQL is a server-client architecture — the server process (mysqld) must be running before any client or application can connect.
The server listens on port 3306 by default — firewall rules, OS-level packet filters, and cloud security groups can silently block connections even when the server is healthy.
MySQL 8.4 LTS is the target version for new projects in 2026 — MySQL 8.0 EOLs in April 2026.
Rule: always verify (1) server process status and (2) port 3306 reachability as the first two steps when debugging any MySQL connection error.
Key Takeaway
MySQL has two pieces: the server (background process mysqld, owns data files, listens on port 3306) and the client (shell or GUI you type queries into).
'MySQL not running' and 'MySQL not installed' are different problems — check server status first before debugging anything else.
MySQL 8.4 LTS is the current stable release for 2026 — use it for all new installations.
Installing MySQL on Windows, macOS, and Linux — Step by Step
MySQL installs differently on each operating system, but the goal is the same: get the server running and the client available in your terminal. Pick your OS below and follow every step — skipping even one (especially the secure installation script) is responsible for the majority of headaches beginners report.
Windows: winget is the cleanest path for developers in 2026 — it's built into Windows 10 and 11, handles the download and service registration silently, and doesn't require you to click through a wizard. If you prefer the graphical installer, it's still available at dev.mysql.com/downloads/installer — choose the 'Server only' variant unless you also want Workbench.
macOS: Homebrew remains the standard developer workflow. If you're on Apple Silicon (M1/M2/M3/M4), Homebrew installs to /opt/homebrew — Homebrew-managed MySQL works natively on ARM, no Rosetta needed.
Linux (Ubuntu/Debian): The apt package manager handles this cleanly on Ubuntu 22.04 and 24.04. Note that Ubuntu 24.04's default apt repository ships MySQL 8.0 — if you specifically want MySQL 8.4, you'll need to add Oracle's official APT repository first. On RHEL 9 and Fedora, use dnf.
After the install, regardless of your OS, you'll run mysql_secure_installation — a built-in script that locks down the default configuration. This is not optional. The default MySQL install (even 8.4) leaves certain things in a permissive state that you must harden. The script does it in about two minutes.
mysql_install_all_platforms.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# ════════════════════════════════════════════════════════
# OPTION A — macOS (using Homebrew)
# ════════════════════════════════════════════════════════
# Step1: InstallHomebrewif you don't have it
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# After install on AppleSilicon, follow the prompt to add Homebrew to your PATH.
# Step2: InstallMySQL8.4 via Homebrew
brew install mysql
# As of 2026, 'brew install mysql' resolves to MySQL8.4LTS.
# Homebrew installs under /opt/homebrew/opt/mysql/ on AppleSilicon.
# Step3: Start the MySQL server now AND on every future boot
brew services start mysql
# 'start' = launch now + register as a login item (auto-start on reboot).
# Use'brew services run mysql'if you only want it running forthis session.
# Verify it started correctly:
brew services list | grep mysql
mysql --version
# ════════════════════════════════════════════════════════
# OPTION B — Ubuntu22.04 / 24.04 (MySQL8.4 from Oracle repo)
# ════════════════════════════════════════════════════════
# Step1: AddOracle's official MySQLAPT repository
# This ensures you get MySQL8.4LTS, not 8.0 from Ubuntu's default repo.
wget https://dev.mysql.com/get/mysql-apt-config_0.8.32-1_all.deb
sudo dpkg -i mysql-apt-config_0.8.32-1_all.deb
# A dialog appears — select MySQL8.4Server, then OK.
# Step2: Refresh the package list to include Oracle's repo
sudo apt update
# Step3: InstallMySQL8.4Server
sudo apt install mysql-server -y
# The service starts automatically after install.
# Step4: Verify version and service status
mysql --version
sudo systemctl status mysql
# Step5: Enable auto-start on system reboot (usually already enabled)
sudo systemctl enable mysql
# ════════════════════════════════════════════════════════
# OPTION C — Windows10 / 11 (via winget)
# ════════════════════════════════════════════════════════
# OpenPowerShell as Administrator:
winget install Oracle.MySQL
# winget downloads MySQL8.4, installs silently, and registers a WindowsService.
# After install completes, verify:
Get-Service -Name'MySQL*'
mysql --version
# ════════════════════════════════════════════════════════
# ALLPLATFORMS — Security hardening script (run this next, always)
# ════════════════════════════════════════════════════════
sudo mysql_secure_installation
# OnWindows, run this from the MySQL bin directory or add it to your PATH.
# The script will ask you five questions — answer as shown:
# 1. Set up VALIDATEPASSWORD component? → YES (enforces strong passwords)
# 2. Password strength level → 1 (MEDIUM) is fine for local dev, 2for production
# 3. Set a root password? → YES — pick something strong, write it down
# 4. Remove anonymous users? → YES
# 5. Disallow root login remotely? → YES
# 6. Remove test database? → YES
# 7. Reload privilege tables now? → YES
# After it finishes, verify you can log in with the new password:
mysql -u root -p
# You should reach the 'mysql>' prompt after entering your root password.
Output
# macOS: brew services list
mysql started homebrew.mxcl.mysql
# mysql --version output (all platforms):
mysql Ver 8.4.4 Distrib 8.4.4, for Linux (x86_64) using EditLine wrapper
# Ubuntu apt install (tail of output):
Setting up mysql-server-8.4 (8.4.4-1ubuntu22.04) ...
Created symlink /etc/systemd/system/multi-user.target.wants/mysql.service →
/lib/systemd/system/mysql.service.
Starting MySQL Community Server ...
# mysql_secure_installation — full interactive session:
Securing the MySQL server deployment.
Enter password for user root:
VALIDATE PASSWORD COMPONENT can be used to test passwords
and improve security.
Estimated strength of the password: 100
Do you wish to continue with the password provided? (Press y|Y for Yes) : y
Remove anonymous users? (Press y|Y for Yes): y
... Success!
Disallow root login remotely? (Press y|Y for Yes): y
... Success!
Remove test database and access to it? (Press y|Y for Yes): y
- Dropping test database...
... Success!
Reload privilege tables now? (Press y|Y for Yes): y
... Success!
All done!
Watch Out: macOS 'Access Denied' After Homebrew Install
On a fresh Homebrew MySQL 8.4 install, the root user authenticates via the caching_sha2_password plugin — which on a brand-new install with no password set means you connect with sudo mysql (no -p flag) rather than mysql -u root -p. If the latter gives 'Access denied', try sudo mysql first. Once inside, run: ALTER USER 'root'@'localhost' IDENTIFIED BY 'YourNewPassword'; FLUSH PRIVILEGES; — this sets the password while keeping the modern auth plugin. Then you can connect with mysql -u root -p going forward.
Production Insight
Default MySQL installs — even 8.4 — leave anonymous users and the test database in place until you remove them.
mysql_secure_installation is a two-minute investment that closes the most commonly exploited default weaknesses.
On Ubuntu 24.04, apt installs MySQL 8.0 by default — add Oracle's APT repo to get 8.4 LTS.
Rule: run mysql_secure_installation immediately after install on every machine, every time — no exceptions for dev environments.
Key Takeaway
Install method varies by OS but the goal is the same: MySQL 8.4 server running, client in your PATH.
mysql_secure_installation removes anonymous users, sets root password, enables password validation, and deletes the test database — it is not optional.
On Ubuntu 24.04, explicitly add Oracle's APT repo to get MySQL 8.4 instead of the older 8.0 from Ubuntu's default packages.
Creating Your First Database, User, and Table — Then Querying It
With MySQL running and secured, let's do something concrete. We'll create a database for a bookstore, add a dedicated MySQL user (never use root for application connections — more on that in the common mistakes section), create a table for books, insert some rows, and run SELECT queries to prove it all works.
Think of a database as a named folder on your filing cabinet. A table is a spreadsheet inside that folder. A row is one record — one book. A column is one field present in every record — title, author, price. The structure of the columns is defined when you create the table, and MySQL enforces it on every row you insert.
One decision worth making consciously before you create any table: character set. We use utf8mb4 below, not the older utf8. MySQL's 'utf8' is actually a broken three-byte subset of real UTF-8 that cannot store emoji or certain Asian scripts. utf8mb4 is proper four-byte UTF-8 — it handles everything. In MySQL 8.4, utf8mb4 is the default, so you may not need to specify it explicitly, but being explicit is a good habit when your data will cross environments.
The SQL commands below work identically on MySQL 8.4 and MariaDB 10.11+. Type them one at a time in your MySQL shell, or save them as a .sql file and run them with mysql -u root -p < bookstore_setup.sql.
bookstore_setup.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
-- ── STEP 1: Connect to MySQL as root ──────────────────────────────────────-- Run this in your terminal (outside MySQL):-- mysql -u root -p-- Enter the root password you set during mysql_secure_installation.-- You'll see the 'mysql>' prompt when you're inside.-- ── STEP 2: Create a dedicated database ───────────────────────────────────CREATEDATABASEIFNOTEXISTS bookstore_db
CHARACTERSET utf8mb4 -- full Unicode including emojiCOLLATE utf8mb4_unicode_ci; -- case-insensitive string comparison-- IF NOT EXISTS prevents an error if you run this script twice.-- Verify it was created:SHOWDATABASES;
-- ── STEP 3: Create a dedicated application user ────────────────────────────-- Replace 'secure_password_here' with a real password (16+ chars, mixed case,-- numbers, symbols). The user only has access from this machine ('localhost').CREATEUSERIFNOTEXISTS'bookstore_app'@'localhost'IDENTIFIEDBY'secure_password_here';
-- IF NOT EXISTS prevents an error if you accidentally run this twice.-- ── STEP 4: Grant only the permissions the app actually needs ──────────────GRANTSELECT, INSERT, UPDATE, DELETEON bookstore_db.*
TO'bookstore_app'@'localhost';
-- The app gets no DROP, CREATE, GRANT, or SHOW DATABASES.-- If the app is ever compromised, the attacker cannot destroy the schema-- or create backdoor users.-- Apply the privilege changes immediately:FLUSHPRIVILEGES;
-- ── STEP 5: Switch into the bookstore database ────────────────────────────USE bookstore_db;
-- ── STEP 6: Create the books table ───────────────────────────────────────CREATETABLEIFNOTEXISTSbooks (
book_id INTNOTNULL AUTO_INCREMENT,
title VARCHAR(255) NOTNULL,
author_name VARCHAR(150) NOTNULL,
genre VARCHAR(80) NOTNULL,
price_usd DECIMAL(8,2) NOTNULL, -- exact decimal, never FLOAT for money
in_stock BOOLEANNOTNULLDEFAULTTRUE,
created_at TIMESTAMPNOTNULLDEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPNOTNULLDEFAULT CURRENT_TIMESTAMP
ONUPDATE CURRENT_TIMESTAMP,
PRIMARYKEY (book_id),
INDEXidx_genre (genre), -- speeds up WHERE genre = '...'INDEXidx_price (price_usd) -- speeds up ORDER BY price_usd
);
-- AUTO_INCREMENT: MySQL assigns book_id automatically — 1, 2, 3, ...-- DECIMAL(8,2): stores values up to 999999.99 exactly — right for money.-- ON UPDATE CURRENT_TIMESTAMP: updated_at refreshes automatically on any change.-- The two INDEX definitions are small now but matter when the table has millions of rows.-- ── STEP 7: Insert sample books ───────────────────────────────────────────INSERTINTObooks (title, author_name, genre, price_usd) VALUES
('The Pragmatic Programmer', 'David Thomas', 'Technology', 49.95),
('Clean Code', 'Robert C. Martin', 'Technology', 35.00),
('Dune', 'Frank Herbert', 'Sci-Fi', 14.99),
('Project Hail Mary', 'Andy Weir', 'Sci-Fi', 16.50),
('The Psychology of Money', 'Morgan Housel', 'Finance', 18.00);
-- We omit book_id, in_stock, created_at, updated_at — MySQL fills these automatically.-- ── STEP 8: Query the data ────────────────────────────────────────────────-- All books, cheapest first:SELECT book_id, title, author_name, price_usd
FROM books
ORDERBY price_usd ASC;
-- Only Sci-Fi books:SELECT title, author_name, price_usd
FROM books
WHERE genre = 'Sci-Fi';
-- Count and average price per genre:SELECT
genre,
COUNT(*) AS total_books,
AVG(price_usd) AS avg_price,
MIN(price_usd) AS cheapest,
MAX(price_usd) AS most_expensive
FROM books
GROUPBY genre
ORDERBY total_books DESC, avg_price DESC;
-- ── STEP 9: Verify the table structure ────────────────────────────────────DESCRIBE books;
-- Shows column names, types, nullability, defaults, and key assignments.-- Good habit: always describe a table after creating it to confirm it looks right.
Storing prices as FLOAT is one of the most common beginner mistakes and one of the most expensive to fix later. Floating-point arithmetic cannot exactly represent most decimal fractions — $12.99 stored as FLOAT may come back as $12.98999977111816 in your application. Multiply that across thousands of transactions and you have a genuine accounting error. DECIMAL(8,2) stores exact values with exactly 2 decimal places — it works the way money actually works. The '8' is the total digit count, so DECIMAL(8,2) handles up to $999,999.99. Need bigger? Use DECIMAL(12,2) for values up to $9,999,999,999.99.
Production Insight
Never use root for application connections — if the app is compromised, root access means total server takeover.
Use IF NOT EXISTS in CREATE DATABASE and CREATE USER — makes your setup scripts safe to re-run without errors.
Index columns you filter or sort on from day one (genre, price_usd) — adding indexes to a table with 10 million rows later causes a full table lock in older MySQL versions.
Rule: one dedicated user per application, least-privilege GRANT, utf8mb4 character set, DECIMAL for money.
Key Takeaway
Create a dedicated MySQL user per application with minimum GRANT permissions — SELECT, INSERT, UPDATE, DELETE only.
Use IF NOT EXISTS on CREATE DATABASE and CREATE USER so your scripts are idempotent.
Use DECIMAL(8,2) for money — FLOAT introduces silent rounding errors that compound across transactions.
Add indexes on columns you know you'll filter or sort on — far easier to do at creation time than at scale.
Connecting MySQL to Your Application — And Verifying Everything Works
A database that only lives in the MySQL shell isn't much use. Let's connect it from real application code. We'll use Python with the mysql-connector-python library as the example — the connection concepts (host, port, user, password, database, commit) apply identically in Node.js, PHP, Go, Java, and every other language. The library is the translator: your Python code calls Python functions, the library converts them into MySQL's wire protocol, sends them to the MySQL server over a TCP socket, gets results back, and hands them to Python as normal objects.
Notice we connect as bookstore_app — not root. This is a habit you want to build from day one, not something you bolt on later. If someone finds a SQL injection vulnerability in your code (it happens to experienced engineers too), a connection with only SELECT/INSERT/UPDATE/DELETE on one database cannot drop your tables, read your system tables, or create backdoor users. Root can do all of that. The scope of a security incident is directly determined by the privilege level of the compromised connection.
For mysql-connector-python in 2026, make sure you're on version 9.x — it has native support for MySQL 8.4's caching_sha2_password authentication plugin, which is the default in 8.4. Older versions of the connector (8.0.x) may throw an authentication error against MySQL 8.4 servers. The fix is simply upgrading: pip install --upgrade mysql-connector-python.
The connection credentials — host, port, user, password, database — should always come from environment variables in real projects. Never hardcode passwords in source files. Source files end up in Git. Git history is forever.
bookstore_db_connect.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# Install the connector first (run in your terminal, not in Python):# pip install mysql-connector-python# Verify you have a 9.x version for MySQL 8.4 compatibility:# pip show mysql-connector-pythonimport mysql.connector
import os
from decimal importDecimal# ── Connection configuration — from environment variables ─────────────────# Before running locally, set these in your shell:# export DB_HOST=127.0.0.1# export DB_PASSWORD=secure_password_here# In production, inject these from a secrets manager or CI/CD secret variables.
db_config = {
"host": os.getenv("DB_HOST", "127.0.0.1"), # 127.0.0.1 is safer than 'localhost'"port": int(os.getenv("DB_PORT", "3306")),
"user": os.getenv("DB_USER", "bookstore_app"),
"password": os.getenv("DB_PASSWORD", ""), # empty default forces explicit env var"database": os.getenv("DB_NAME", "bookstore_db"),
}
# Note: we use 127.0.0.1 rather than 'localhost' deliberately.# On Linux, 'localhost' routes through a Unix socket, 127.0.0.1 uses TCP.# The bookstore_app user was created for 'localhost' (socket) but if your# driver resolves differently, the host spec in mysql.user may not match.# Using 127.0.0.1 consistently avoids this ambiguity.defget_connection():
"""
Returns a new MySQL connection. Call this at the start of each operation.
In a real application, replace this with a connection pool
(e.g., mysql.connector.pooling.MySQLConnectionPool).
"""
return mysql.connector.connect(**db_config)
deffetch_books_under_price(max_price: Decimal) -> list[dict]:
"""
Returns all books priced strictly below max_price, sorted cheapest first.
Uses a parameterised query — NEVER concatenate user input into SQL strings.
"""
withget_connection() as connection:
# dictionary=True: each row comes back as {'title': 'Dune', 'price_usd': Decimal('14.99')}# rather than a plain tuple — much easier to work with in application code.with connection.cursor(dictionary=True) as cursor:
query = """
SELECT book_id, title, author_name, price_usd
FROM books
WHERE price_usd < %s
ORDERBY price_usd ASC"""
# The %s placeholder is replaced safely by the connector library.# Parameters are always passed as a tuple — even a single parameter.# This prevents SQL injection because user input never becomes SQL syntax.
cursor.execute(query, (max_price,))
return cursor.fetchall()
defadd_new_book(title: str, author: str, genre: str, price: Decimal) -> int:
"""
Inserts a new book row and returns the auto-generated book_id.
Demonstrates the commit() pattern required for all write operations.
"""
withget_connection() as connection:
with connection.cursor() as cursor:
insert_query = """
INSERTINTObooks (title, author_name, genre, price_usd)
VALUES (%s, %s, %s, %s)
"""
cursor.execute(insert_query, (title, author, genre, price))
# ── CRITICAL: commit() writes the transaction to disk ─────────# Without this line, the INSERT is held in a pending InnoDB# transaction. When the 'with' block exits and the connection# closes, MySQL silently rolls back the pending transaction.# No error. No warning. The row simply will not exist.
connection.commit()
# lastrowid gives us the AUTO_INCREMENT value MySQL assigned.return cursor.lastrowid
defupdate_book_price(book_id: int, new_price: Decimal) -> bool:
"""
Updates the price of a book by ID.
ReturnsTrueif a row was updated, Falseif book_id was not found.
"""
withget_connection() as connection:
with connection.cursor() as cursor:
cursor.execute(
"UPDATE books SET price_usd = %s WHERE book_id = %s",
(new_price, book_id)
)
connection.commit() # required — this is a write operation
return cursor.rowcount > 0# rowcount = number of rows actually changed# ── Demo: run when the script is executed directly ───────────────────────if __name__ == "__main__":
print("=== Books under $20 ===")
affordable = fetch_books_under_price(max_price=Decimal("20.00"))
for book in affordable:
print(f" [{book['book_id']}] {book['title']} by {book['author_name']} — ${book['price_usd']}")
print("\n=== Adding a new book ===")
new_id = add_new_book(
title="Atomic Habits",
author="James Clear",
genre="Self-Help",
price=Decimal("16.99")
)
print(f" New book inserted — book_id = {new_id}")
print("\n=== Updating a price ===")
updated = update_book_price(book_id=new_id, new_price=Decimal("14.99"))
print(f" Price update {'succeeded'if updated else'failed — book_id not found'}")
Output
=== Books under $20 ===
[3] Dune by Frank Herbert — $14.99
[4] Project Hail Mary by Andy Weir — $16.50
[5] The Psychology of Money by Morgan Housel — $18.00
=== Adding a new book ===
New book inserted — book_id = 6
=== Updating a price ===
Price update succeeded
Watch Out: Forgetting connection.commit() Silently Loses Your Data
This is the single most common mistake I see from developers who are new to MySQL — and it is absolutely maddening to debug because there is no error. You call cursor.execute(), it returns without complaint, cursor.rowcount says '1 row affected', and then you query the table and the row is not there. What happened: MySQL's InnoDB storage engine (the default since MySQL 5.5) uses transactions. Every INSERT, UPDATE, or DELETE starts a transaction implicitly. That transaction stays pending until you explicitly call connection.commit(). When the connection closes — either at the end of your 'with' block or when the script exits — MySQL sees an uncommitted transaction and rolls it back. Silently. The fix is always the same: add connection.commit() after every write. If you want MySQL to commit automatically after each statement without explicit calls, set autocommit=True in your connection config — but be aware this disables the ability to roll back a failed batch of writes.
Production Insight
Use mysql-connector-python 9.x for MySQL 8.4 — older 8.0.x versions may fail against caching_sha2_password auth.
Use 127.0.0.1 rather than 'localhost' in connection strings to avoid socket vs. TCP ambiguity on Linux.
Parameterised queries (%s placeholders) are the only correct way to include variable data in SQL — string concatenation is never acceptable.
Rule: commit() after every write, environment variables for every credential, parameterised queries for every user input.
Key Takeaway
Use mysql-connector-python 9.x for MySQL 8.4 compatibility — upgrade with pip install --upgrade mysql-connector-python.
Never hardcode passwords in source code — use environment variables or a secrets manager.
Parameterised queries (%s) prevent SQL injection — never concatenate user input into SQL strings.
connection.commit() is required after every INSERT, UPDATE, and DELETE — InnoDB holds writes in a pending transaction until you explicitly commit.
● Production incidentPOST-MORTEMseverity: high
Production Database Exposed to the Internet Because mysql_secure_installation Was Skipped
Symptom
Customer data appeared on a public paste site. The application was still functioning normally — no visible errors, no alerts fired. The breach was discovered by a third-party security researcher who found the server responding to connections on port 3306 from arbitrary IPs. By the time the team was notified, the attacker had been inside for an estimated 11 hours.
Assumption
The team assumed MySQL was secure by default after installation and that only the application server could reach the database because they shared the same VPC. No one had explicitly verified the bind-address setting or tested port 3306 reachability from outside the VPC.
Root cause
The MySQL root account had no password (default behaviour on Ubuntu apt install prior to 8.4). The server was bound to 0.0.0.0 — accepting connections from any IP — instead of 127.0.0.1. The test database and anonymous users were still present from the default install. A port scan from the public internet discovered port 3306 open because a security group rule had been left overly permissive during a debugging session two weeks earlier and never reverted. The attacker connected as root with no password and dumped all databases in under four minutes.
Fix
1. Immediately terminated all active MySQL connections and revoked the exposed security group rule at the cloud provider level
2. Ran mysql_secure_installation to set a strong root password and remove anonymous users
3. Changed bind-address in /etc/mysql/mysql.conf.d/mysqld.cnf from 0.0.0.0 to 127.0.0.1 and restarted the service
4. Added an explicit deny firewall rule: ufw deny 3306 from any
5. Created dedicated users per application with least-privilege permissions and rotated all application credentials
6. Enabled MySQL audit logging (using the Community audit plugin) for future detection
7. Added automated port-scan checks to the CI/CD pipeline to catch accidental 3306 exposure before it reaches staging
Key lesson
Always run mysql_secure_installation immediately after installing MySQL — no exceptions, no 'we'll do it before go-live'
Never bind MySQL to 0.0.0.0 unless you specifically need remote access and have verified, tested firewall rules protecting it
Block port 3306 at the firewall or security group level independently of the bind-address setting — defense in depth
Default MySQL installs (especially pre-8.4) are intentionally permissive by design — hardening is your responsibility, not the installer's
Audit your security group and firewall rules on a schedule — one-off 'temporary' debug changes have a way of becoming permanent
Production debug guideWhen your app cannot connect to MySQL or queries behave unexpectedly5 entries
Symptom · 01
Can't connect to MySQL server on '127.0.0.1' (errno 111 — Connection refused)
→
Fix
The MySQL server is not running or not listening on port 3306. Check with: sudo systemctl status mysql. If inactive, start it with: sudo systemctl start mysql. If it starts and immediately stops, something is wrong with the configuration — check /var/log/mysql/error.log for the actual reason. On MySQL 8.4, a common cause is a datadir permissions issue after an upgrade from 8.0.
Symptom · 02
Access denied for user 'app_user'@'localhost' (using password: YES)
→
Fix
The user exists but the password is wrong, or the user was created for a different host specifier. On MySQL 8.4, also check that the client library supports caching_sha2_password — older versions of mysql-connector-python (below 8.0.28) may fail silently against 8.4's default auth plugin. Verify with: SELECT user, host, plugin FROM mysql.user; and try connecting as root to confirm the server is reachable.
Symptom · 03
INSERT rows disappear — no error, SELECT returns nothing
→
Fix
Missing connection.commit(). InnoDB uses transactions — uncommitted changes roll back when the connection closes. Add connection.commit() after every write operation. If you want writes to auto-commit without explicit calls, set autocommit=True in the connection config, but understand you lose the ability to roll back a failed batch.
Symptom · 04
Too many connections error (max_connections exceeded)
→
Fix
Check the current setting: SHOW VARIABLES LIKE 'max_connections'; — the default is 151 on most installs. Also check how many connections are currently open: SHOW STATUS LIKE 'Threads_connected'; If you are using a connection pool in your application, verify the pool size is not exceeding max_connections across all app instances. Look for connection leaks — connections that are opened but never closed, especially in error paths.
Symptom · 05
Authentication plugin 'caching_sha2_password' cannot be loaded
→
Fix
This appears when an older client library connects to MySQL 8.4, which uses caching_sha2_password by default instead of mysql_native_password. Update your client library first: pip install --upgrade mysql-connector-python. If you cannot upgrade the library, you can change a specific user's auth plugin: ALTER USER 'app_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password'; — but treat this as a temporary workaround, not a long-term fix.
★ MySQL Setup Debugging Cheat SheetWhen MySQL installation or connection goes wrong — quick commands to find the problem fast
MySQL server not responding−
Immediate action
Check if the server process is actually running before touching anything else
Commands
sudo systemctl status mysql
sudo lsof -i :3306
Fix now
Start the server: sudo systemctl start mysql. If it starts then immediately stops, the error is in the config or datadir — check: sudo journalctl -u mysql --no-pager -n 50 and /var/log/mysql/error.log for the real reason.
Access denied for root user+
Immediate action
Try socket-based auth first — on Ubuntu and macOS Homebrew installs, root uses socket auth by default, not password
Commands
sudo mysql
SELECT user, host, plugin FROM mysql.user WHERE user='root';
Fix now
If plugin is auth_socket or caching_sha2_password and you need password login, run: ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'NewPass123!'; FLUSH PRIVILEGES; — note that MySQL 8.4 deprecates mysql_native_password so prefer caching_sha2_password with an updated client.
Connection refused from application code+
Immediate action
Verify MySQL is listening on the address and port your application code is targeting
If bind-address is 127.0.0.1, connect to 127.0.0.1 in your application code — not 'localhost', which on Linux resolves to a Unix socket path, not the TCP loopback interface. This is a surprisingly common mismatch.
MySQL 8.4 LTS (Community) vs MySQL Workbench (GUI)
Aspect
MySQL 8.4 LTS (Community)
MySQL Workbench (GUI)
What it is
The actual database server (mysqld) plus the mysql CLI client
A visual desktop application for managing MySQL via point-and-click
Required?
Yes — this IS the database. Nothing works without it.
No — entirely optional, but genuinely useful for beginners exploring data visually
How you interact
Terminal commands and SQL queries at the mysql> prompt
Drag, click, and write queries in a colour-coded visual editor
Best for
Production use, scripting, automation, CI/CD pipelines
Schema design, one-off exploratory queries, visual data browsing, ER diagrams
Resource usage
Lightweight server process — roughly 200-400MB RAM depending on buffer pool config
Workbench 8.0.36+ supports MySQL 8.4 — verify version before connecting
Install method
apt / Homebrew / winget — covered in this guide
Separate download from dev.mysql.com/downloads/workbench
Needed to follow this guide?
Yes — install this first
No — install it once you're comfortable with the CLI, as a complement not a replacement
Key takeaways
1
MySQL has two pieces
the server (background process mysqld, owns the data files, listens on port 3306) and the client (shell or GUI you type queries into). Both must be present; the server must be running before any client or application can connect.
2
MySQL 8.4 LTS is the current stable release for 2026. MySQL 8.0 reaches end-of-life in April 2026
use 8.4 for all new installations and plan upgrades for existing 8.0 deployments.
3
Always run mysql_secure_installation immediately after installing MySQL
it removes anonymous users, sets a root password, enables password validation, and deletes the test database. Skipping it leaves avoidable attack surface on every machine you touch.
4
Create a dedicated MySQL user per application with only the permissions it needs
GRANT SELECT, INSERT, UPDATE, DELETE on that app's database. Root is for administration only, never for application connections.
5
INSERT, UPDATE, and DELETE require connection.commit() in Python (and most languages) before changes are written to disk
InnoDB uses transactions by default, and uncommitted changes are silently rolled back when the connection closes.
6
Use DECIMAL(8,2) for money columns
FLOAT uses binary floating-point arithmetic that cannot exactly represent most decimal fractions, causing rounding errors like $12.98999977 that accumulate across transactions.
7
Never hardcode database credentials in source code
use environment variables for local development and a secrets manager in production. Credentials that appear in Git history should be treated as permanently compromised and rotated immediately.
8
Update mysql-connector-python to version 9.x when using MySQL 8.4
older versions do not support the caching_sha2_password authentication plugin that MySQL 8.4 uses by default.
Common mistakes to avoid
6 patterns
×
Using root for application database connections
Symptom
The app works fine locally. But any SQL injection vulnerability — even a minor one — gives an attacker root-level access to the entire MySQL server. They can DROP every table, CREATE backdoor users, and read the mysql system tables. The blast radius is total.
Fix
Always create a dedicated MySQL user per application with only the permissions that application needs: GRANT SELECT, INSERT, UPDATE, DELETE ON your_db.* TO 'app_user'@'localhost'; Root credentials belong in your password manager for administration tasks, never in application config files or environment variables that application processes read.
×
Forgetting connection.commit() after INSERT/UPDATE/DELETE
Symptom
Code runs without errors. cursor.rowcount reports '1 row affected'. But a subsequent SELECT returns nothing — the row has vanished. This is especially confusing when the script runs in development with autocommit enabled (some ORMs set this) but not in production where the connection pool has different defaults.
Fix
Add connection.commit() immediately after every write operation. Check your ORM or connection pool settings explicitly — don't assume autocommit is on or off. If you want MySQL to commit each statement automatically without explicit calls, set autocommit=True in the connection config and document that decision clearly.
×
Skipping mysql_secure_installation after setup
Symptom
MySQL root has no password. Anonymous users exist. The 'test' database is present and accessible to anyone who can reach port 3306. If the server is even briefly exposed to the internet — or if another user on the same machine is malicious — the database is wide open.
Fix
Run sudo mysql_secure_installation immediately after installing MySQL — before you do anything else. Answer yes to every prompt. If you already skipped it, you can run it again at any time on a running server — it is completely safe to re-run and does not affect existing databases or users you've already created.
×
Storing money as FLOAT instead of DECIMAL
Symptom
A price stored as $12.99 comes back from the database as $12.98999977111816 in your application. Calculations on financial values produce penny-level rounding errors that accumulate silently across thousands of transactions — sometimes in your favour, sometimes not, never correctly.
Fix
Use DECIMAL(8,2) for all money columns, no exceptions. DECIMAL stores exact values with a fixed number of decimal places, which is how money actually works. FLOAT uses IEEE 754 binary floating-point, which physically cannot represent most decimal fractions exactly. This is not a MySQL quirk — it's a fundamental property of binary arithmetic.
×
Hardcoding database credentials in source code
Symptom
The password appears in plain text in a .py, .js, or .php file that gets committed to a Git repository. Git history is permanent — even if you delete the file later, the password remains in history. If the repository is ever made public, or if an attacker gains read access, the database is immediately compromised.
Fix
Use environment variables: os.getenv('DB_PASSWORD') in Python, process.env.DB_PASSWORD in Node.js. Add .env to .gitignore before making your first commit. In production, use a proper secrets manager — AWS Secrets Manager, HashiCorp Vault, or your CI/CD platform's built-in secret variables. Treat any credential that has ever been committed to source control as permanently compromised and rotate it immediately.
×
Connecting to MySQL 8.4 with an outdated client library
Symptom
Application throws: 'Authentication plugin caching_sha2_password cannot be loaded' or 'MySQL Client does not support authentication protocol'. The server is running fine, credentials are correct, but the connection fails immediately.
Fix
Upgrade your client library: pip install --upgrade mysql-connector-python for Python (target 9.x). For Node.js, upgrade mysql2 to 3.x. If you absolutely cannot upgrade the library in the short term, you can change a specific user to the older plugin: ALTER USER 'app_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password'; — but schedule the library upgrade, because mysql_native_password is deprecated in MySQL 8.4 and will be removed in a future version.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01JUNIOR
What is the difference between the MySQL server and the MySQL client, an...
Q02JUNIOR
Why should you never use the root MySQL account for application database...
Q03SENIOR
A developer inserts 1,000 rows in a loop using Python's mysql-connector-...
Q04SENIOR
How would you secure a MySQL server that is already in production but wa...
Q05SENIOR
An application worked correctly against MySQL 8.0 but fails with an auth...
Q01 of 05JUNIOR
What is the difference between the MySQL server and the MySQL client, and why does that distinction matter when debugging a 'connection refused' error?
ANSWER
The MySQL server is the mysqld background process that owns the data files on disk and listens for incoming connections on port 3306. The MySQL client is the mysql CLI tool (or any GUI, or your application's database library) that connects to the server over a TCP socket or Unix socket. When debugging 'connection refused' (errno 111 on Linux), the distinction matters because the error means the client cannot reach the server — not that the credentials are wrong. Three things to verify in order: (1) is the server process running — sudo systemctl status mysql; (2) is something listening on port 3306 — sudo ss -tlnp | grep 3306; (3) is a firewall blocking the port — sudo ufw status or check cloud security groups. Only move on to credential debugging after you've confirmed all three.
Q02 of 05JUNIOR
Why should you never use the root MySQL account for application database connections, and what permissions would you grant to a least-privilege app user instead?
ANSWER
Root has unrestricted global access — it can DROP any table, CREATE or DELETE any user, read the mysql system tables, and shut down the server. If a SQL injection vulnerability exists anywhere in the application (and at scale they eventually do), a root connection hands an attacker total control of the database server. A least-privilege app user should receive only: GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_user'@'localhost'; — nothing else. This limits the damage from a compromised connection to data manipulation within that one database. The app doesn't need DROP, CREATE, ALTER, or GRANT, so it shouldn't have them.
Q03 of 05SENIOR
A developer inserts 1,000 rows in a loop using Python's mysql-connector-python but none of the rows appear in a subsequent SELECT — no errors were thrown. What is the most likely cause and how do you fix it?
ANSWER
The most likely cause is a missing connection.commit() call after the insert loop. MySQL's InnoDB storage engine (the default) uses transactions — every INSERT, UPDATE, or DELETE is held in a pending transaction until commit() is called explicitly. When the Python script exits or the connection closes, MySQL sees an open uncommitted transaction and automatically rolls it back. No error, no warning, no rows. The fix is to add connection.commit() after the insert loop completes. Alternatively, set autocommit=True in the connection config to commit each statement individually — but understand this means you cannot roll back a failed batch. You can verify the root cause during debugging by checking SHOW ENGINE INNODB STATUS for active transactions before the connection closes.
Q04 of 05SENIOR
How would you secure a MySQL server that is already in production but was never hardened after installation?
ANSWER
All of the following can be done on a running production server without downtime or restarting MySQL:
1. Set a strong root password: ALTER USER 'root'@'localhost' IDENTIFIED BY 'StrongPass123!';
2. Remove anonymous users: DELETE FROM mysql.user WHERE User='';
3. Remove remote root access: DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');
4. Drop the test database: DROP DATABASE IF EXISTS test; DELETE FROM mysql.db WHERE Db='test' OR Db='test\_%';
5. Reload the grant tables: FLUSH PRIVILEGES;
6. Audit all existing users and their privileges: SELECT user, host, plugin, password_expired FROM mysql.user;
7. Audit the bind-address setting in my.cnf — if it is 0.0.0.0, change it to 127.0.0.1 and restart the server in a scheduled maintenance window.
8. Add firewall rules to block port 3306 from any external IP, regardless of bind-address.
9. Enable MySQL's audit log plugin for ongoing detection of suspicious queries.
Document every change with a timestamp and the reason — security hardening of a running server should go through your change management process.
Q05 of 05SENIOR
An application worked correctly against MySQL 8.0 but fails with an authentication error after upgrading the database server to MySQL 8.4. The credentials have not changed. What is happening and how do you resolve it?
ANSWER
MySQL 8.4 uses caching_sha2_password as the default authentication plugin, replacing mysql_native_password which was the default in 8.0. Older client libraries — mysql-connector-python 8.0.x, older versions of mysqljs, JDBC drivers below 8.0.28 — do not support caching_sha2_password and fail immediately when connecting to an 8.4 server. The correct resolution is to upgrade the client library to a version that supports caching_sha2_password — mysql-connector-python 9.x for Python, mysql2 3.x for Node.js. As a temporary workaround (not a permanent fix, since mysql_native_password is deprecated and will be removed), you can switch specific users back: ALTER USER 'app_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password'; FLUSH PRIVILEGES; — but schedule the library upgrade immediately. Note that mysql_native_password is deprecated in MySQL 8.4 and the deprecation warning will appear in your server error log if it is still in use.
01
What is the difference between the MySQL server and the MySQL client, and why does that distinction matter when debugging a 'connection refused' error?
JUNIOR
02
Why should you never use the root MySQL account for application database connections, and what permissions would you grant to a least-privilege app user instead?
JUNIOR
03
A developer inserts 1,000 rows in a loop using Python's mysql-connector-python but none of the rows appear in a subsequent SELECT — no errors were thrown. What is the most likely cause and how do you fix it?
SENIOR
04
How would you secure a MySQL server that is already in production but was never hardened after installation?
SENIOR
05
An application worked correctly against MySQL 8.0 but fails with an authentication error after upgrading the database server to MySQL 8.4. The credentials have not changed. What is happening and how do you resolve it?
SENIOR
FAQ · 6 QUESTIONS
Frequently Asked Questions
01
How do I install MySQL 8.4 on Windows 11 without the graphical wizard?
Open PowerShell as Administrator and run: winget install Oracle.MySQL — winget (the Windows Package Manager, built into Windows 10 and 11) handles the download, silent installation, and Windows Service registration automatically. After it finishes, verify with: Get-Service -Name 'MySQL*' — you should see a Running status. Then open a new PowerShell window and run: mysql --version to confirm the client is in your PATH. If winget resolves to MySQL 8.0, search specifically: winget search Oracle.MySQL and install by the exact package identifier for 8.4.
Was this helpful?
02
What is the default MySQL root password after installation?
It depends on the OS and install method. On Ubuntu via apt (and with Homebrew on macOS), there is no password by default — root authenticates via the auth_socket or caching_sha2_password plugin with an empty password, which means you connect with sudo mysql (no -p flag). The mysql_secure_installation script is what sets an actual password. On Windows via winget or the official installer wizard, you set the root password during installation. On MySQL 8.4 specifically, the temporary password generated during installation on some platforms is written to /var/log/mysql/error.log — run: sudo grep 'temporary password' /var/log/mysql/error.log to find it if you missed it.
Was this helpful?
03
What is the difference between MySQL and MariaDB — can I follow this guide for both?
MariaDB is a community fork of MySQL created in 2009 by MySQL's original developers after Oracle acquired MySQL. For everything covered in this guide — installation, mysql_secure_installation, basic SQL, user creation, GRANT syntax, connecting from Python — they are fully interchangeable. The CLI commands, SQL syntax, and connector libraries work identically at this level. Differences appear in more advanced territory: JSON function behaviour, certain storage engines, replication configuration, and some performance-related system variables. If you are on MariaDB 10.11 LTS (the current stable MariaDB release as of 2026), every command in this guide works exactly as written.
Was this helpful?
04
What is the difference between localhost and 127.0.0.1 when connecting to MySQL?
On Linux and macOS, 'localhost' tells the MySQL client to connect via a Unix socket file (typically /var/run/mysqld/mysqld.sock or /tmp/mysql.sock) — bypassing the TCP network stack entirely. '127.0.0.1' tells the client to use TCP/IP on the loopback interface. MySQL treats these as completely different connection types, and importantly, as different hosts when matching user privileges. A user created with 'app_user'@'localhost' has a socket-based host specifier. If your application connects via TCP to 127.0.0.1, MySQL may see this as a different host and deny access. In practice: use 127.0.0.1 consistently in your application connection strings for predictable TCP behavior, and create users with 'localhost' if you want them to use socket auth, or '127.0.0.1' if you want TCP.
Was this helpful?
05
How do I reset the MySQL root password if I forgot it?
On Linux: 1. Stop the MySQL service: sudo systemctl stop mysql 2. Start MySQL without privilege checking: sudo mysqld_safe --skip-grant-tables --skip-networking & 3. Connect without a password: mysql -u root 4. Reload grants first: FLUSH PRIVILEGES; 5. Set the new password: ALTER USER 'root'@'localhost' IDENTIFIED BY 'NewPassword123!'; 6. Exit the MySQL shell: EXIT; 7. Kill the unsafe mysqld process: sudo pkill -f mysqld_safe && sudo pkill -f mysqld 8. Start MySQL normally: sudo systemctl start mysql 9. Verify the new password works: mysql -u root -p
On macOS (Homebrew): same process, but use brew services stop mysql and brew services start mysql instead of systemctl.
On Windows: stop the MySQL84 service in services.msc, then follow the MySQL 8.4 documentation for the --init-file approach to reset the password without skip-grant-tables.
Was this helpful?
06
My application worked with MySQL 8.0 but now gives an authentication error after upgrading to MySQL 8.4. What changed?
MySQL 8.4 uses caching_sha2_password as the default authentication plugin. MySQL 8.0 used mysql_native_password by default. Older client libraries do not support caching_sha2_password. Fix: upgrade your client library first — pip install --upgrade mysql-connector-python for Python (you want 9.x). For Node.js, upgrade mysql2 to version 3.x. If you cannot upgrade the library immediately, you can change specific users back to the older plugin as a temporary measure: ALTER USER 'app_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password'; FLUSH PRIVILEGES; — but note that mysql_native_password is deprecated in MySQL 8.4 and will generate warnings in your error log, so treat library upgrade as the permanent solution.