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

How to use a MySQL database with an aiogram bot (aiomysql)

Telegram bot hostingUpdated Nov 6, 202612 min read

Short answer. Add aiomysql to your aiogram 3 bot, open one pool with autocommit=True before polling starts, and hand it to your handlers with dp.start_polling(bot, pool=pool): aiogram passes it to every handler that has a pool parameter. Use %s placeholders and store Telegram IDs as BIGINT, not INT. 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 aiogram 3.31.0, 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) and a stand-in for Telegram's Bot API server: 8 commands in, 8 answers out, the notes saved in the database. A real Telegram login was not part of the test.

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. The bot token is not one of these lines: the token you saved on the Bot page, pane Telegram, already reaches your code as BOT_TOKEN, the name aiogram's own examples use (and as TELEGRAM_BOT_TOKEN, TOKEN, TELEGRAM_TOKEN and TELOXIDE_TOKEN). If your .env file has a token line, the dialog skips it. More on this pane: How to set environment variables and secrets.

3. The code

A small notes bot: /note buy milk saves a note, /notes shows your last five.

requirements.txt

aiogram>=3,<4
aiomysql

bot.py

import asyncio
import logging
import os
import sys

import aiomysql
from aiogram import Bot, Dispatcher
from aiogram.filters import Command, CommandObject
from aiogram.types import Message

dp = Dispatcher()


@dp.message(Command("note"))
async def note(message: Message, command: CommandObject, pool: aiomysql.Pool) -> None:
    text = (command.args or "").strip()
    if not text:
        await message.answer("Write your note after the command, for example: /note buy milk")
        return
    if len(text) > 500:
        await message.answer("A note can be up to 500 characters.")
        return
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute(
                "INSERT INTO notes (user_id, text) VALUES (%s, %s)",
                (message.from_user.id, text),
            )
    await message.answer("Saved. Send /notes to see your last five.")


@dp.message(Command("notes"))
async def notes(message: Message, pool: aiomysql.Pool) -> None:
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute(
                "SELECT text FROM notes WHERE user_id = %s ORDER BY id DESC LIMIT 5",
                (message.from_user.id,),
            )
            rows = await cur.fetchall()
    if not rows:
        await message.answer("You have no notes yet.")
        return
    await message.answer("\n".join(f"- {text}" for (text,) in rows))


async def main() -> None:
    # One pool for the whole bot. The values come from the Environment pane.
    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 pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute(
                "CREATE TABLE IF NOT EXISTS notes ("
                "id INT AUTO_INCREMENT PRIMARY KEY, "
                "user_id BIGINT NOT NULL, "
                "text VARCHAR(500) NOT NULL, "
                "created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "
                "KEY user_id (user_id))"
            )
    print("Database ready")

    bot = Bot(os.environ["BOT_TOKEN"])
    try:
        # Every keyword passed here reaches the handlers that ask for it by name.
        await dp.start_polling(bot, pool=pool)
    finally:
        pool.close()
        await pool.wait_closed()


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, stream=sys.stdout)
    asyncio.run(main())

What it does: main() opens the pool, creates the notes table if it is missing and prints Database ready before the bot asks Telegram for messages. dp.start_polling(bot, pool=pool) is aiogram's own way to share an object: every extra keyword you pass becomes data that aiogram hands to any handler with a parameter of that name, so note() and notes() get the same pool. When the bot stops, the finally block closes the pool.

The %s placeholders are filled in by the driver, so a note such as '); DROP TABLE notes; -- is stored as plain text. In our test it was saved as row 2 and the table stayed. Never build the SQL with an f-string.

Store Telegram IDs as BIGINT

Telegram's Bot API documentation says a user or chat ID "may have more than 32 significant bits" but has at most 52, so a 64-bit integer is safe. Its page on IDs gives the ranges: user IDs go up to 1,099,511,627,775, while group, supergroup and channel IDs are negative. Supergroups and channels run from -1,000,000,000,001 to -1,997,852,516,352 (for example -1001234567890). MySQL's INT stops at 2,147,483,647. In our test, with the same strict SQL mode our database server uses, saving the user ID 7,000,000,001 in an INT column failed. aiomysql raised:

pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1")

A BIGINT column stored it, and -1001234567890 too. Use plain BIGINT, not BIGINT UNSIGNED, so chat IDs fit as well.

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, Database ready and Run polling for bot @your_bot. That last line turns the status on Overview to Online in Telegram.
  3. In Telegram, send your bot /note buy milk, then /notes. To look at the rows, open the database's page and click Open in phpMyAdmin.

From the second start on, the console also shows Warning: Table 'notes' already exists, with the path of aiomysql's cursors.py in front of it. That is MariaDB's note for CREATE TABLE IF NOT EXISTS, passed on by the driver. Nothing is wrong.

In our test the stand-in for Telegram sent these commands and got these answers:

SentAnswer
/note buy milkSaved. Send /notes to see your last five.
/noteWrite your note after the command, for example: /note buy milk
/note and 501 charactersA note can be up to 500 characters.
/notesThe saved notes, newest first
/notes from a second userYou have no notes yet.

Many messages at once

aiogram handles each incoming update as its own task, so several commands can run their queries at the same time. In our test, 4 commands that arrived together were each handled in about 50 ms. maxsize=5 caps the pool at 5 connections; a sixth query waits for a free one instead of opening another. After the test, the bot held 2 open connections.

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.

A failed query does not stop the bot

In our test we deleted the table while the bot was running and sent /notes. aiogram logged the error with a traceback ending in pymysql.err.ProgrammingError: (1146, "Table '...notes' doesn't exist"), sent no answer for that command, and kept running: once the table was back, the next /note was saved. The person who sent the command sees nothing, so if a query can fail, catch the error in the handler and answer with a short message.

A quiet night does not break the pool

Our database server closes a connection that has been idle for 8 hours. A bot that gets no messages for that long, overnight for example, finds its pooled connections closed. aiomysql checks for that when a handler asks for a connection and opens a new one in place of a closed one. We tested it with the idle limit lowered to 5 seconds: after 7 quiet seconds, the next /note was saved with no error.

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 an earlier test of ours, on an account set up the same way, 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 errors aiomysql gave in our test, written the way they end a traceback in the console (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.
  • pymysql.err.OperationalError: (1226, ... 'max_user_connections' ...): too many connections open, see the limits above. For example a second copy of the bot, or a pool opened for each message.
  • pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1"): an ID column is INT. Change it to BIGINT.
  • pymysql.err.ProgrammingError: (1146, "Table '...' doesn't exist"): the table is missing. The code above creates it at start; if you made it by hand, check its name.

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.

A wrong token shows up after the database part. With our fake token against the real Telegram, the bot printed Database ready, then stopped with aiogram.exceptions.TelegramUnauthorizedError: Telegram server says - Unauthorized. Copy the token again from @BotFather and paste it on the Bot page, pane Telegram. More Telegram errors: Fix Telegram bot 409 Conflict.

How much memory it uses

In our test the running bot used 174 MB of memory (resident) after answering the 8 commands, with its pool open, in a container capped at 512 MB like our smallest bot plan. Most of that is aiogram itself: in our earlier hosting test, aiogram alone measured 173 MB (that bot stopped at a refused token, before handling any message, so the two numbers are close but not an exact comparison). Installing aiogram and aiomysql with pip from nothing peaked at 163 MB, file cache included. That leaves room for your own code on the smallest plan. The hosting guide compares the libraries: python-telegram-bot vs aiogram on PlayNHost.

Same thing with another library

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

Already hosting your bot somewhere else? Move it to us

OUR LATEST BLOGS