A database for your Discord bot: saving points, levels and settings with SQLite
Your bot counts points, remembers the welcome channel or each user's reminders and, when you restart it, everything is back to zero. That's because it keeps it in memory. Here's how to save it in a file that survives restarts, with SQLite, in Python and in JavaScript.
Why your bot forgets everything when it restarts
What you keep in a variable (a dictionary of points in Python, an object or a Map in JavaScript) lives in the program's memory. When the bot restarts, because you stop it, because it updates or because it crashes, the program starts from scratch and that memory is gone.
For something to last, it has to be written to disk: to a file the bot reads when it starts. The simplest thing that works well is SQLite, a database that is a single file, with no separate server to install or look after.
Rather not set it up yourself? Discord bot hosting from €1.50 a month, no contract and a full refund within 72 hours. Or create a free Discord bot, no card needed.
JSON, SQLite or an external database
A JSON file is fine for a few settings that hardly ever change (the prefix, the log channel). For data that changes all the time, like points or levels, it falls short: you have to rewrite the whole file on every change, two commands at once can overwrite each other and, if the bot stops right while writing, the file can be left half-written and never open again.
SQLite saves each change completely or not at all (transactions), stays fast even with thousands of users and comes with Python (the sqlite3 module) and with Node.js 22.13 onwards and 24 (the node:sqlite module). For a bot on one Discord server or on hundreds, it's the best fit.
An external database (MongoDB, PostgreSQL, MySQL on another service) makes sense when several programs have to read and write the same data at once, for example your bot and a website with the leaderboard. In return, it's another account, another password to keep safe, and every query travels over the internet.
In Python, with sqlite3
Nothing to install: sqlite3 comes with Python. This bot has two commands, /point, which gives you a point, and /points, which tells you how many you have. Stop it and start it again: the points are still there. The requirements.txt and the .env are the ones from your first bot in Python.
sqlite3 is synchronous: while it runs a query, the bot waits. With small queries like these you won't notice; if one day you run heavy queries, the aiosqlite library does the same without blocking it.
import os
import sqlite3
from pathlib import Path
import discord
from discord import app_commands
from dotenv import load_dotenv
load_dotenv()
# The file sits next to app.py, so it doesn't matter which folder the bot starts from.
DB_PATH = Path(__file__).with_name("data.db")
db = sqlite3.connect(DB_PATH)
db.execute("CREATE TABLE IF NOT EXISTS points (user_id INTEGER PRIMARY KEY, total INTEGER NOT NULL DEFAULT 0)")
db.commit()
client = discord.Client(intents=discord.Intents.default())
tree = app_commands.CommandTree(client)
@tree.command(name="point", description="Gives you one point")
async def point(interaction: discord.Interaction):
with db: # saves when the block ends; if something fails, nothing is half-saved
db.execute(
"INSERT INTO points (user_id, total) VALUES (?, 1) "
"ON CONFLICT(user_id) DO UPDATE SET total = total + 1",
(interaction.user.id,),
)
await interaction.response.send_message("➕ One more point")
@tree.command(name="points", description="How many points you have")
async def points(interaction: discord.Interaction):
row = db.execute("SELECT total FROM points WHERE user_id = ?", (interaction.user.id,)).fetchone()
await interaction.response.send_message(f"You have {row[0] if row else 0} points")
@client.event
async def on_ready():
await tree.sync()
print(f"Logged in as {client.user}")
client.run(os.environ["DISCORD_TOKEN"])In JavaScript, with node:sqlite
Since Node.js 22.13, and in Node.js 24, SQLite is built in as node:sqlite, nothing to install. On startup the console says it's an experimental feature (ExperimentalWarning): that's normal and the bot works just the same. The package.json and the .env are the ones from your first bot in JavaScript.
Store each user's ID as text: Discord IDs are bigger than JavaScript can hold as a number without rounding, and discord.js already gives them to you as text.
Stuck on Node.js 20? node:sqlite isn't there: add the better-sqlite3 library to package.json; the way you use it (prepare, run, get) is almost the same as in this example.
require("dotenv").config();
const path = require("node:path");
const { DatabaseSync } = require("node:sqlite"); // built into Node.js 22.13+ and 24
const { Client, Events, GatewayIntentBits, SlashCommandBuilder } = require("discord.js");
// The file sits next to index.js, so it doesn't matter which folder the bot starts from.
const db = new DatabaseSync(path.join(__dirname, "data.db"));
db.exec("CREATE TABLE IF NOT EXISTS points (user_id TEXT PRIMARY KEY, total INTEGER NOT NULL DEFAULT 0)");
const add = db.prepare(
"INSERT INTO points (user_id, total) VALUES (?, 1) ON CONFLICT(user_id) DO UPDATE SET total = total + 1",
);
const read = db.prepare("SELECT total FROM points WHERE user_id = ?");
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const commands = [
new SlashCommandBuilder().setName("point").setDescription("Gives you one point"),
new SlashCommandBuilder().setName("points").setDescription("How many points you have"),
];
client.once(Events.ClientReady, async (c) => {
await c.application.commands.set(commands.map((x) => x.toJSON()));
console.log(`Logged in as ${c.user.tag}`);
});
client.on(Events.InteractionCreate, async (i) => {
if (!i.isChatInputCommand()) return;
if (i.commandName === "point") {
add.run(i.user.id); // the ID as text: Discord IDs don't fit in a JavaScript number
await i.reply("➕ One more point");
}
if (i.commandName === "points") {
const row = read.get(i.user.id);
await i.reply(`You have ${row ? row.total : 0} points`);
}
});
client.login(process.env.DISCORD_TOKEN);Where the file goes, and what not to push to GitHub
Put the database next to the code, with the path taken from the program's own folder (Path(__file__) in Python, __dirname in JavaScript), as in the examples. With a bare path like “data.db”, the file is created in whatever folder the bot is started from, and if that ever changes, the bot starts with a new, empty database and it looks like it lost everything.
If your code is on GitHub, add data.db to .gitignore (and the .env too). The database isn't code: it holds your users' IDs and changes by itself every time someone uses a command. If it's in the repository, every code update can clash with the bot's copy or overwrite it.
.env
data.db
data.db-*Backing up the database
Copying the file while the bot is running can give you a half-written copy if it was writing at that moment. Either stop the bot before copying it, or make the copy from the program itself: in Python, with backup(), which copies correctly even while the bot is writing; in SQL, with “VACUUM INTO 'backup.db'”, which also works in node:sqlite.
If you see files ending in -wal and -shm next to your database, it's using WAL mode: they're part of it and, if you copy by hand, copy all three together.
import sqlite3
# A good copy even while the bot is running and writing.
source = sqlite3.connect("data.db")
target = sqlite3.connect("backup.db")
with target:
source.backup(target)
target.close()
source.close()If your bot is on NapNodes
The file stays on the bot's disk
What your bot writes in its folder stays there when you stop it, restart it and update the code: you can see it in “Files”. A bot's database needs very little disk: the free bot has 1 GB, Mini 2.5 GB and Plus 5 GB, and a points database for thousands of users is only a few megabytes.
If your code comes from GitHub
With update on every restart turned on in “Settings”, the panel pulls the changes from your repository. With data.db in .gitignore, as above, your database isn't touched.
Backups
On a plan, in “Backups” you can back up the whole bot (database included), and the “Copia diaria automática” (the automatic daily backup) is made by itself every night. The free bot has no backups: stop it and download data.db from “Files” every now and then.
Before reinstalling or deleting
Download data.db from “Files” before “Reinstall server” (at the bottom of “Settings”) or before deleting the bot. And if yours is the free bot, renew it every week: if you don't, it's stopped, and after 14 days without renewing it's deleted with all its files (we warn you first).
Only store what the bot needs
For points, levels or reminders, the user's ID and the Discord server's ID are enough: no need to store their messages, their name (it changes) or anything else. The less you store, the less you have to protect. And collecting user data for anything other than what your bot does is against Discord's rules and against our Acceptable Use Policy.
Frequently asked questions
JSON or SQLite for a Discord bot?
JSON for a few settings that hardly change; SQLite for anything that changes often, like points, levels or reminders. SQLite isn't left half-written if the bot stops while writing, and it comes with Python and with Node.js 22.13 onwards.
Why does my bot lose its data when it restarts?
Because it keeps it in variables, which live in memory and are wiped on restart. You have to write it to a file, for example an SQLite database next to the code.
Is my bot's database deleted when I restart it on NapNodes?
No: the files the bot writes in its folder stay on its disk and you can see them in “Files”. Before “Reinstall server” or deleting the bot, download it.
I get “database is locked”, what do I do?
Another program has the same file open and is writing. The most common cause is running the bot twice (on your computer and on the host at the same time), or a transaction that never closes: in Python, save with “with db:” or with commit() after each change.
Can I use MongoDB with my bot?
Yes, if your bot can connect to that service over the internet. It pays off when several programs need to share the data; for a bot on its own, SQLite is simpler and doesn't depend on another account.
Shall we set it up for you?
Discord bot hosting from €1.50 a month, never oversold: your RAM is yours alone. And if you'd rather start without paying, there's a free Discord bot, no card needed.
Keep reading
- Discord Leveling Bot: XP for Messages, Leaderboard and Roles (Python and JavaScript)
Make your own Discord leveling bot in Python or JavaScript: spam-proof XP for messages, /level, /leaderboard and roles on level-up, stored in SQLite.
- Discord Bot in TypeScript (discord.js): From Zero to Always Online
Your Discord bot in TypeScript with discord.js: package.json, tsconfig, typed slash commands, compiling with tsc and hosting it always online, for free.
- Discord Verification Bot in Python: a Button and a Question Before Joining
Build your own Discord verification bot with discord.py: a “Verify me” button, an anti-bot question and a role that unlocks the server. Full code, free.