MySQL Create User: Syntax, Grants & Examples


Our MySQL database support team creates and audits database accounts on client servers daily. This page documents the CREATE USER statement and the MySQL commands that surround it: host patterns, GRANT syntax, the permission sets used for application, reporting, backup, and replication accounts, password management, and account removal. All statements are as documented in the MySQL 8.4 reference manual.
CREATE USER syntax
The basic form, per the CREATE USER reference:
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'password-here';
Common variants:
-- Fail silently if the account already exists
CREATE USER IF NOT EXISTS 'appuser'@'localhost' IDENTIFIED BY 'password-here';
-- Specify the authentication plugin explicitly
CREATE USER 'appuser'@'localhost'
IDENTIFIED WITH caching_sha2_password BY 'password-here';
-- Force a password change at first login
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'temp-password' PASSWORD EXPIRE;
-- Limit resource use
CREATE USER 'reporting'@'10.0.0.%' IDENTIFIED BY 'password-here'
WITH MAX_QUERIES_PER_HOUR 500 MAX_USER_CONNECTIONS 5;
CREATE USER requires the global CREATE USER privilege, or the INSERT privilege for the mysql system schema. Creating a user grants it no privileges beyond USAGE (connect only); privileges are added separately with GRANT.
The 'user'@'host' part
A MySQL account is the combination of a user name and a host, not the user name alone. 'appuser'@'localhost' and 'appuser'@'10.0.0.5' are two separate accounts with separate passwords and separate privileges. The host part controls where the client may connect from, as documented in Specifying Account Names:
| Host value | Meaning |
|---|---|
localhost | Connections from the local machine only, via the Unix socket or 127.0.0.1 depending on client settings. The default when the host part is omitted is %, not localhost — write the host explicitly. |
% | Any host. Required for some application setups, but it means the password is the only barrier; combine it with firewall rules. |
10.0.0.5 | One specific IP address. |
10.0.0.% | Any host in 10.0.0.0/24. The % and _ wildcards work in host names and IP addresses. |
10.0.0.0/255.255.255.0 | Subnet in address/netmask form. Only netmasks with contiguous leading 1-bits are valid. |
app01.example.com | A host name. Matching depends on reverse DNS of the client address, which adds a lookup and a failure mode; IP-based host values are more predictable. |
When a client connects, the server picks the most specific matching account row. A connection from the local machine matches 'appuser'@'localhost' before 'appuser'@'%', so two such accounts with different passwords produce access-denied errors that look intermittent. Avoid creating both unless there is a reason.
Authentication plugins
caching_sha2_password has been the default authentication plugin since MySQL 8.0. Accounts created without an IDENTIFIED WITH clause use it. The older mysql_native_password plugin was deprecated in 8.0, disabled by default in 8.4, and removed in MySQL 9.0, per the native pluggable authentication page. Old client libraries and connectors that only speak mysql_native_password fail against 9.x servers; the fix is updating the client library, not downgrading the account. On 8.4, mysql_native_password can still be enabled with --mysql-native-password=ON for transition purposes.
GRANT syntax and privilege levels
The GRANT reference defines four privilege levels, set by the ON clause:
GRANT ALL PRIVILEGES ON *.* TO 'admin2'@'localhost'; -- global
GRANT SELECT, INSERT ON appdb.* TO 'appuser'@'localhost'; -- database
GRANT SELECT ON appdb.orders TO 'reporting'@'10.0.0.%'; -- table
GRANT SELECT (id, email) ON appdb.users TO 'audit'@'localhost'; -- column
Rules that matter in practice:
- The account must already exist. GRANT does not create accounts (the implicit-create behavior was removed in MySQL 8.0; it now returns
ERROR 1410when NO_AUTO_CREATE_USER conditions are not met — use CREATE USER first). - Granting requires holding the same privileges yourself, plus the GRANT OPTION privilege, or the UPDATE privilege on the grant tables.
WITH GRANT OPTIONlets the account grant its own privileges onward. Application accounts do not need it.- Database-level wildcards exist (
ON app%.*) but thepartial_revokessystem variable changes how they behave; explicit database names are simpler to audit.
Common permission sets
| Account type | Grant |
|---|---|
| Application user | GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'appuser'@'10.0.0.%'; Add EXECUTE if the application calls stored procedures. Schema-migration steps need CREATE, ALTER, DROP, INDEX, REFERENCES — grant those to a separate migration account, not the runtime account. |
| Read-only reporting | GRANT SELECT ON appdb.* TO 'reporting'@'10.0.0.%'; Add SHOW VIEW if the reports query views and the tool inspects their definitions. |
| Backup user (mysqldump) | GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER, PROCESS ON *.* TO 'backup'@'localhost'; PROCESS is required for dumping tablespace information; add RELOAD when using flush-log options. With single-transaction dumps of InnoDB-only data, LOCK TABLES can be dropped. Details in the mysqldump guide. |
| Replication user | GRANT REPLICATION SLAVE ON *.* TO 'repl'@'10.0.0.%'; Per the replication setup documentation, REPLICATION SLAVE is the only privilege the replica needs to read the source binary log. REPLICATION CLIENT additionally allows status statements. |
| Administrator | GRANT ALL PRIVILEGES ON *.* TO 'admin2'@'localhost' WITH GRANT OPTION; Restrict the host to localhost or a management subnet. In 8.x, dynamic privileges (SYSTEM_VARIABLES_ADMIN, BACKUP_ADMIN, and others) are included in ALL when granted at the global level. |
FLUSH PRIVILEGES: when it is needed
Account-management statements — CREATE USER, GRANT, REVOKE, ALTER USER, DROP USER — take effect immediately. The server reloads the grant tables into memory on its own after these statements. FLUSH PRIVILEGES is required only after modifying the grant tables directly with INSERT, UPDATE, or DELETE against the mysql schema, as stated in the privilege change documentation. Running it after every GRANT does nothing except reload tables that are already current. Direct edits to the grant tables are themselves discouraged; use the account-management statements.
Viewing grants
SHOW GRANTS FOR 'appuser'@'10.0.0.%'; -- one account
SHOW GRANTS; -- current account
SELECT user, host, plugin FROM mysql.user; -- list all accounts
SHOW GRANTS output is valid GRANT syntax, which makes it the fastest way to copy a permission set to a new account: run SHOW GRANTS on the old account, edit the account name, execute.
Changing passwords
ALTER USER is the documented statement for password changes (SET PASSWORD exists but ALTER USER is preferred):
ALTER USER 'appuser'@'localhost' IDENTIFIED BY 'new-password';
ALTER USER 'appuser'@'localhost' PASSWORD EXPIRE; -- force change at next login
ALTER USER 'appuser'@'localhost' ACCOUNT LOCK; -- disable without dropping
Password strength rules come from the validate_password component, installed with INSTALL COMPONENT 'file://component_validate_password';. It enforces length, mixed case, numeric, and special-character policies through the validate_password.* system variables (LOW, MEDIUM, STRONG policy levels), per the validate_password documentation. When a CREATE USER or ALTER USER statement fails with ERROR 1819 Your password does not satisfy the current policy requirements, this component is the reason; check SHOW VARIABLES LIKE 'validate_password%';.
Removing users and revoking privileges
REVOKE INSERT, UPDATE, DELETE ON appdb.* FROM 'appuser'@'10.0.0.%'; -- reduce
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'olduser'@'%'; -- strip everything
DROP USER 'olduser'@'%'; -- remove the account
DROP USER IF EXISTS 'olduser'@'%';
DROP USER removes the account and its privilege rows, but it does not close the account's open sessions. An existing connection continues working until it disconnects; kill its sessions (SHOW PROCESSLIST, then KILL id) when removal must be immediate. Objects the dropped user created (tables, views, stored procedures with DEFINER set to it) remain and can produce definer-not-found errors later; reassign definers before dropping accounts that own routines.
Remote access checklist
Three layers must all permit the connection. Check them in this order:
- bind-address. The server must listen on a reachable interface.
bind_address = 127.0.0.1in mysqld configuration limits the server to local connections; set it to the server's LAN address or0.0.0.0and restart mysqld. Verify withSHOW VARIABLES LIKE 'bind_address';. - Firewall. Port 3306 must be open from the client network:
ufw allow from 10.0.0.0/24 to any port 3306or the firewalld/cloud-security-group equivalent. Do not open 3306 to the internet; restrict by source. - Account host. The account's host part must match the client address.
'appuser'@'localhost'cannot connect remotely regardless of the first two layers; create'appuser'@'10.0.0.%'or the appropriate subnet.
Test from the client machine with mysql -h server-ip -u appuser -p. A timeout points at layers 1-2; an immediate error points at layer 3 or the password. Tools such as phpMyAdmin sit behind the same three layers when hosted on a separate machine.
Common errors
| Error | Cause and fix |
|---|---|
ERROR 1045 (28000): Access denied for user | Wrong password, or the user/host pair that matched is not the one expected. Run SELECT user, host FROM mysql.user WHERE user='appuser'; and check which row the client address matches; the message shows the host the server resolved ('appuser'@'app01'). Reset the password with ALTER USER on the exact matching account. |
ERROR 1130 (HY000): Host 'x' is not allowed to connect | No account row matches the client host at all. Create the account with a host pattern covering the client address, or widen an existing account's host with RENAME USER. Also produced when DNS resolves the client to a name that matches no row; skip_name_resolve makes matching IP-only and predictable. |
ERROR 1819 (HY000): password does not satisfy the current policy | validate_password component rejected the password. Meet the policy or lower validate_password.policy deliberately. |
ERROR 1410 (42000): You are not allowed to create a user with GRANT | GRANT was issued for a nonexistent account. Run CREATE USER first, then GRANT. |
ERROR 2059: Authentication plugin cannot be loaded | Old client library without caching_sha2_password support. Update the connector; on 9.x servers mysql_native_password is no longer available as a fallback. |
When to hand it over
Account sprawl — dozens of 'user'@'%' rows with ALL PRIVILEGES, no record of which application uses which — is the normal state of a MySQL server that has passed through several administrators. Our MySQL support team audits accounts against actual connection logs, rebuilds least-privilege grants, and handles user management as part of ongoing server administration. A DBA is available on live chat 24/7.
Our Microsoft-certified team delivers seamless migrations with zero downtime.
Microsoft 365 Migration ServicesTopics

Sreenivasa Reddy G
Founder & CEO • 15+ years
Sreenivasa Reddy is the Founder and CEO of Medha Cloud, recognized as "Startup of the Year 2024" by The CEO Magazine. With over 15 years of experience in cloud infrastructure and IT services, he leads the company's vision to deliver enterprise-grade cloud solutions to businesses worldwide.
More in Hosting Solutions
View all
Apache vs Tomcat: Differences & When to Use Each
9 min read

Node.js Hosting: Options, PM2 & Server Setup
9 min read

Apache Tomcat: Download, Setup & Versions
10 min read

IIS SSL Certificate: Install, Bind & Renew
9 min read

MySQL Support: Oracle Tiers, Contacts & Options
8 min read

MySQL Performance Tuning: Slow Queries, InnoDB & Config
10 min read