NEW 📢 Limited launch offer: up to 70% off your first month on every game!See plans

How to use a MySQL database with a discord.py bot (aiomysql)

Discord bot hostingUpdated Nov 6, 20267 min read

Short answer. Add aiomysql to your discord.py bot, create one pool in setup_hook with autocommit=True, and run every query through it with %s placeholders. Without autocommit=True (or a commit() after each write), aiomysql does not save your INSERTs. On PlayNHost every bot plan includes one MySQL database: create it on the Databases page, paste its details into the Environment pane, and the code below works as it is. The database accepts connections from your PlayNHost servers and phpMyAdmin, not from your own computer. We tested this code on 6 November 2026 with discord.py 2.7.1, aiomysql 0.3.2 and Python 3.12, against MariaDB 10.11 with a database account set up like ours (20 connections, 120-second query limit): the start, the table and the /warn command, but not a real Discord login.

What you need

1. Create the database

  1. Open your bot server in the panel and click Databases.
  2. Click New database. A Label is optional; Will be created as shows the full name. Click Create database.
  3. The new database's page opens (later, click it in Your databases). Its Connection card shows Host, Port, Database, Username and Password. Under Ready-made config, Config for has a Python (aiomysql) snippet with the same values filled in.

2. Put the details in Environment

  1. Open the Bot page, pane Environment, and click Paste a .env file.
  2. Paste these five lines with the values from the Connection card:
    DB_HOST=your-host
    DB_PORT=3306
    DB_NAME=your-database
    DB_USER=your-username
    DB_PASSWORD=your-password
  3. DB_PASSWORD gets a Secret badge: it is saved write-only and never shown again. Click Import 5 variables.

Use the real port from the Connection card. Your code reads the values with os.environ, so the password never sits in your bot's files. More on this pane: How to set environment variables and secrets.

3. The code

requirements.txt

discord.py
aiomysql

bot.py

import os

import aiomysql
import discord
from discord import app_commands


class Bot(discord.Client):
    def __init__(self):
        super().__init__(intents=discord.Intents.default())
        self.tree = app_commands.CommandTree(self)
        self.pool = None

    async def setup_hook(self):
        # One pool for the whole bot. The values come from the Environment pane.
        self.pool = await aiomysql.create_pool(
            host=os.environ["DB_HOST"],
            port=int(os.environ["DB_PORT"]),
            user=os.environ["DB_USER"],
            password=os.environ["DB_PASSWORD"],
            db=os.environ["DB_NAME"],
            autocommit=True,
            maxsize=5,
        )
        async with self.pool.acquire() as conn:
            async with conn.cursor() as cur:
                await cur.execute(
                    "CREATE TABLE IF NOT EXISTS warnings ("
                    "id INT AUTO_INCREMENT PRIMARY KEY, "
                    "guild_id BIGINT UNSIGNED NOT NULL, "
                    "user_id BIGINT UNSIGNED NOT NULL, "
                    "reason VARCHAR(200) NOT NULL, "
                    "created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "
                    "KEY guild_user (guild_id, user_id))"
                )
        print("Database ready")
        await self.tree.sync()

    async def close(self):
        if self.pool is not None:
            self.pool.close()
            await self.pool.wait_closed()
        await super().close()


bot = Bot()


@bot.tree.command(description="Warn a member and count their warnings")
@app_commands.guild_only()
@app_commands.describe(user="Who to warn", reason="Why")
async def warn(interaction: discord.Interaction, user: discord.Member, reason: app_commands.Range[str, 1, 200]):
    async with bot.pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute(
                "INSERT INTO warnings (guild_id, user_id, reason) VALUES (%s, %s, %s)",
                (interaction.guild_id, user.id, reason),
            )
            await cur.execute(
                "SELECT COUNT(*) FROM warnings WHERE guild_id = %s AND user_id = %s",
                (interaction.guild_id, user.id),
            )
            (total,) = await cur.fetchone()
    await interaction.response.send_message(f"{user.mention} has {total} warning(s).")


@bot.event
async def on_ready():
    print(f"Logged in as {bot.user}")


bot.run(os.environ["DISCORD_TOKEN"])

What it does: discord.py runs setup_hook once, while logging in and before any event arrives, so the pool and the warnings table exist before the first command arrives. It then registers /warn with Discord. @app_commands.guild_only() keeps the command out of direct messages, where there is no server to store. Discord IDs fit in BIGINT UNSIGNED. The %s placeholders are filled in by the driver, so a reason such as '); DROP TABLE warnings; -- is stored as plain text. Never build the SQL with an f-string.

4. Run it

  1. Upload bot.py and requirements.txt in Files.
  2. Press Start and open Console. The start installs the packages from requirements.txt, then shows [PlayNHost] Starting your bot and Database ready.
  3. In Discord, type /warn. To look at the rows, open the database's page and click Open in phpMyAdmin.

The autocommit trap

aiomysql opens connections with autocommit off. An INSERT then stays inside an open transaction. When the connection goes back to the pool with that transaction still open, aiomysql closes it, and MariaDB throws the uncommitted row away. In our test, a row inserted on a pool without autocommit=True was seen by 0 other connections. Either pass autocommit=True to create_pool, as above, or call await conn.commit() after your writes.

Limits that matter for your pool

  • 20 connections at once per database user. Every process that uses the same login counts. Connection 21 is refused with (1226, "User '...' has exceeded the 'max_user_connections' resource (current value: 20)"). maxsize=5 keeps the bot well under it.
  • A query that runs longer than 120 seconds is stopped. MariaDB ends it with error 1969, Query execution was interrupted (max_statement_time exceeded). In our test that came after 120.03 seconds. A bot's queries normally take milliseconds, so this only bites a runaway query.
  • 500 MB per database. The Databases page shows how much is used. At 500 MB, writes are stopped until you delete rows.
  • Server backups do not include the database. Export it from phpMyAdmin from time to time.

Errors and what they mean

These are the exact errors aiomysql gave in our test (they come from PyMySQL, which aiomysql uses underneath):

  • pymysql.err.OperationalError (1045, "Access denied for user ..."): wrong DB_USER or DB_PASSWORD. After Reset password on the Connection card the old password stops working at once: replace DB_PASSWORD and restart the bot.
  • pymysql.err.OperationalError (2003, "Can't connect to MySQL server on ..."): wrong host or port, or a connection from outside PlayNHost. The database only accepts connections from PlayNHost servers and phpMyAdmin, so this is also what you get on your own computer.
  • (1226, ... 'max_user_connections' ...): too many connections open, see the limits above.

A variable missing from Environment stops the bot before it connects, with KeyError: 'DB_HOST' or the name of the missing one, because the code reads each value with os.environ[...]. Add it, then restart the bot: variables apply the next time it starts.

More bot errors: Common Discord bot errors and what they mean.

How much memory it adds

Installing discord.py and aiomysql with pip from nothing peaked at 78 MB of memory in our test, in a container capped at 512 MB like our smallest bot plan. How much the running bot needs depends mostly on how many servers it is in: How much RAM does a Discord bot need?

Same thing in JavaScript, or with Prisma

Need a bot server with a database included? See the bot plans.

Already hosting your bot somewhere else? Move it to us

OUR LATEST BLOGS