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.js bot (mysql2)

Discord bot hostingUpdated Nov 6, 20268 min read

Short answer. Add mysql2 to your discord.js bot, create one connection pool from environment variables when the bot starts, and run every query through it with ? placeholders. 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.js 14.28.1, mysql2 3.24.6 and Node.js 22, 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 handler, 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 Node.js (mysql2) 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. Keeping the password out of your code means it never lands in a file or in a Git repository. More on this pane: How to set environment variables and secrets.

3. The code

package.json

{
  "name": "my-discord-bot",
  "private": true,
  "main": "index.js",
  "dependencies": {
    "discord.js": "^14",
    "mysql2": "^3"
  }
}

index.js

const { Client, Events, GatewayIntentBits, InteractionContextType, MessageFlags, REST, Routes, SlashCommandBuilder } = require('discord.js');
const mysql = require('mysql2/promise');

// One pool for the whole bot. The values come from the Environment pane.
const pool = mysql.createPool({
  host: process.env.DB_HOST,
  port: Number(process.env.DB_PORT),
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  connectionLimit: 5,
});

const commands = [
  new SlashCommandBuilder()
    .setName('warn')
    .setDescription('Warn a member and count their warnings')
    .setContexts(InteractionContextType.Guild)
    .addUserOption((o) => o.setName('user').setDescription('Who to warn').setRequired(true))
    .addStringOption((o) => o.setName('reason').setDescription('Why').setRequired(true).setMaxLength(200))
    .toJSON(),
];

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

client.once(Events.ClientReady, async () => {
  await new REST().setToken(process.env.DISCORD_TOKEN).put(Routes.applicationCommands(client.user.id), { body: commands });
  console.log(`Logged in as ${client.user.tag}`);
});

client.on(Events.InteractionCreate, async (interaction) => {
  if (!interaction.isChatInputCommand() || interaction.commandName !== 'warn' || !interaction.guildId) return;
  const user = interaction.options.getUser('user', true);
  const reason = interaction.options.getString('reason', true);
  try {
    await pool.execute('INSERT INTO warnings (guild_id, user_id, reason) VALUES (?, ?, ?)', [interaction.guildId, user.id, reason]);
    const [rows] = await pool.execute('SELECT COUNT(*) AS total FROM warnings WHERE guild_id = ? AND user_id = ?', [interaction.guildId, user.id]);
    await interaction.reply(`${user} has ${rows[0].total} warning(s).`);
  } catch (err) {
    console.error(err);
    if (!interaction.replied) await interaction.reply({ content: 'Could not save the warning. Try again later.', flags: MessageFlags.Ephemeral }).catch(console.error);
  }
});

async function main() {
  await pool.query(`CREATE TABLE IF NOT EXISTS warnings (
    id INT AUTO_INCREMENT PRIMARY KEY,
    guild_id VARCHAR(20) NOT NULL,
    user_id VARCHAR(20) NOT NULL,
    reason VARCHAR(200) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    KEY guild_user (guild_id, user_id)
  )`);
  console.log('Database ready');
  await client.login(process.env.DISCORD_TOKEN);
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

What it does: when the bot starts it creates the warnings table if it is missing, prints Database ready, then logs in. /warn stores a warning and answers with the member's total. discord.js gives Discord IDs as strings, because they are too large for a JavaScript number, so they are stored as text. The ? placeholders send the member's text to the database separately from the SQL, so a reason such as '); DROP TABLE warnings; -- is stored as plain text.

Two lines keep the bot alive. setContexts(InteractionContextType.Guild) and the !interaction.guildId check stop /warn in a direct message, where there is no server to store. The try/catch answers with a short private message when a query fails. Without it, discord.js hands the error to its error event, and when nothing listens there Node.js stops the bot. In our test we deleted the table and ran /warn: the bot logged ER_NO_SUCH_TABLE, answered Could not save the warning. Try again later. and kept running.

4. Run it

  1. Upload index.js and package.json in Files. Do not upload node_modules.
  2. Press Start and open Console. The start installs the packages, then shows [PlayNHost] Starting your bot, Database ready and Logged in as ....
  3. In Discord, type /warn. To look at the rows, open the database's page and click Open in phpMyAdmin.

Limits that matter for your pool

  • 20 connections at once per database user. Every process that uses the same login counts: your bot, a second copy of it, a dashboard. Connection 21 is refused with ER_USER_LIMIT_REACHED. In our test a pool with connectionLimit: 30 failed with that error as soon as 30 queries ran at once. A pool of 5 never opens more than 5: extra queries wait for a free connection.
  • 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 messages mysql2 gave in our test:

  • ER_ACCESS_DENIED_ERROR ... 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.
  • ECONNREFUSED: nothing accepted the connection at that host and port. Check DB_HOST and DB_PORT. A missing DB_HOST also ends here, because mysql2 then connects to localhost, where no database runs: the variable is missing from Environment, or the bot was not restarted after adding it.
  • ENOTFOUND: DB_HOST is misspelled, so the name does not exist.
  • ETIMEDOUT: nothing answered within 10 seconds, mysql2's default wait. Whatever the message, a connection from your own computer fails: the database only accepts connections from PlayNHost servers and phpMyAdmin.
  • ER_USER_LIMIT_REACHED ... has exceeded the 'max_user_connections' resource (current value: 20): too many connections open, see the limits above. Usually a new connection per command that is never closed, or a second copy of the bot.

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

How much memory it adds

Installing discord.js and mysql2 from nothing peaked at 147 MB of memory (file cache not counted) in our test, in a container capped at 512 MB like our smallest bot plan. From its start until Discord refused our test token, the bot used at most 31 MB; it had set up the table but never logged in. A bot that is logged in and in many servers uses more: How much RAM does a Discord bot need?

Same thing in Python, 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