Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

How to Connect Flask to a MySQL Database: The Minimum Code That Works, the Right Way to Handle Credentials, and Why SQLAlchemy Is Worth the Import

Sean

Platform Writer

Jun 17, 2026
9 min read

To connect Flask to a MySQL database, install a driver (PyMySQL is the cleanest pure-Python choice), point SQLAlchemy at the MySQL URI with SQLALCHEMY_DATABASE_URI, and read the credentials from environment variables — never from app.config. That is the modern answer. The classic Stack Overflow answer is to use flask-mysql and configure a global MySQL object. That answer is from 2014, it conflates the driver and the ORM, and it is the reason most Flask-MySQL tutorials produce code that does not survive a real deploy.

This post covers the path that works in 2026. The first half is the minimum code. The second half is the parts the minimum code skips: the connection pool, the credentials, the migration story, and the failure modes that only show up under load.

The thing most “Flask MySQL” tutorials skip is that the connection is the easy part. The hard part is the lifetime of the connection, the ownership of the credentials, and the answer to “where do the migrations live?” Those are the questions that decide whether the project survives contact with production.

How to Connect Flask to a MySQL Database: The Minimum Code That Works, the Right Way to Handle Credentials, and Why SQLAlchemy Is Worth the Import

Table of contents

The direct answer

The minimum working setup:

pip install Flask Flask-SQLAlchemy PyMySQL

In your app:

import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ["DATABASE_URL"]
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)

The DATABASE_URL should be a standard SQLAlchemy URI:

mysql+pymysql://app_user:[email protected]:3306/app_db

The user, password, host, and database name come from the environment, not from a Python file. The driver is pymysql because it is pure Python, has no compile step, and works on every PaaS deploy without surprise.

That is the working baseline. The rest of this post is the parts the baseline skips and the reasons those parts matter.

Pick a driver first: PyMySQL vs mysqlclient

There are two practical MySQL drivers for Python. The choice between them is the first decision, and most tutorials make it for you without telling you.

mysqlclient is a C extension that wraps the official libmysqlclient C library. It is faster than PyMySQL on raw query throughput — measurably so for large result sets. The cost is that it has to be compiled against the system MySQL libraries, which means a build-essential and libmysqlclient-dev on the build image, and a different set of wheels for every Python version. On a PaaS deploy with a slim Linux base, this is a 200MB image because of the build tools.

PyMySQL is pure Python. There is no compile step. The wheel installs cleanly on Alpine, Debian, Ubuntu, macOS, and Windows. The throughput is slightly lower than mysqlclient, but the difference is in the single-digit percent for most workloads, and the operational simplicity is worth the trade.

For a new project, use PyMySQL. The performance gap is not the bottleneck; the deployment simplicity is the win. If a profile later proves the driver is the bottleneck, swap to mysqlclient. The SQLAlchemy URI is the only thing that changes (mysql+pymysql:// becomes mysql+mysqldb://).

The other drivers — mysql-connector-python from Oracle, aiomysql for async — are fine, but they are specialized. The 90% case is PyMySQL on top of SQLAlchemy.

The minimum SQLAlchemy connection

SQLAlchemy gives Flask a session lifecycle, a connection pool, and a declarative ORM, all from the same setup. The minimum:

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(255), unique=True, nullable=False)
    created_at = db.Column(db.DateTime, server_default=db.func.now())

# in app factory
db.init_app(app)

# create the tables
with app.app_context():
    db.create_all()

The session is request-scoped. You pull a record with User.query.filter_by(email=...).first(), you commit with db.session.commit(), and Flask-SQLAlchemy handles the connection lifecycle for you. You almost never call connect() directly. That is the win.

The pattern that breaks is to use Flask-SQLAlchemy for queries but reach for raw connections for everything else. Two connection paths, two connection pools, two sources of bugs. Pick one and stick to it.

Why environment variables, not config

The credentials should never be in app.config["SQLALCHEMY_DATABASE_URI"] as a string literal. They should be in an environment variable that the runtime reads at startup.

The reasons are operational, not ideological:

  • The same code runs in different places. Local dev uses one MySQL; staging uses another; production uses a third. The same Python file should work in all three.
  • Credentials rotate. The MySQL password should rotate on a schedule. If the password is in a config file, rotating it means a code change and a deploy. If the password is in an env var, rotating it means a platform setting change and a restart.
  • The config file gets committed. Even if you git rm the credentials, the history has them. The team has to remember to scrub the history, and they will not.
  • The deploy platform already has a place for secrets. The platform has a secret store. The env var is the bridge between the secret store and the app.

A clean pattern:

import os
from urllib.parse import quote_plus

DB_USER = os.environ["DB_USER"]
DB_PASSWORD = os.environ["DB_PASSWORD"]
DB_HOST = os.environ["DB_HOST"]
DB_NAME = os.environ["DB_NAME"]

app.config["SQLALCHEMY_DATABASE_URI"] = (
    f"mysql+pymysql://{DB_USER}:{quote_plus(DB_PASSWORD)}@"
    f"{DB_HOST}:3306/{DB_NAME}"
)

quote_plus escapes the password for the URI, which matters if the password contains +, @, :, or /. Without it, a password with @ in it silently breaks the URI parsing, and the error message is Access denied for user 'app_user'@'10.0.0.5' (using password: NO) — which is technically true, but not in the way the engineer thinks.

For the deploy side, a platform that exposes the secret store as environment variables at runtime is the missing piece. The developer writes the code, the platform injects the secrets, the runtime reads them. The credentials never appear in a config file, never in a commit, never in a log.

The connection pool that prevents the 3am page

The default Flask-SQLAlchemy connection pool is a QueuePool with a small default size (5 connections, 10 overflow). For a low-traffic app that is fine. For an app under load, the pool fills up, requests start timing out, and the service degrades in a way that looks like a database problem but is actually a pool exhaustion problem.

The fix is to size the pool against the expected concurrency and the MySQL max_connections:

app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {
    "pool_size": 10,
    "max_overflow": 20,
    "pool_timeout": 30,
    "pool_recycle": 1800,  # recycle connections after 30 min
    "pool_pre_ping": True, # validate connection before use
}

pool_pre_ping=True is the line that prevents the most common production failure: the connection was idle too long, MySQL closed it, the pool handed it out, the next query failed with MySQL server has gone away. Pre-ping sends a cheap SELECT 1 before each checkout; if it fails, the connection is dropped and a new one is created. The cost is one round-trip per checkout; the win is the absence of an unexplained 500 every few hours.

pool_recycle is the belt-and-suspenders version. It rotates connections before the MySQL wait_timeout would close them, so pre-ping is rarely needed. Most teams use both.

For a deeper look at how connection pools, deploy concurrency, and database max_connections interact, the hosting cost calculator gives a useful sanity check: the cheapest deploy that crashes the pool every Friday is not actually cheap.

Migrations: the part everyone skips

db.create_all() is the right call for the first deploy. It is the wrong call for the second. The first schema change after launch will fail, and the team’s first reaction will be to delete the database and start over. That works in dev. It is a page in production.

The fix is migrations from day one. Flask-Migrate (a thin wrapper around Alembic) is the standard choice:

pip install Flask-Migrate
from flask_migrate import Migrate

migrate = Migrate(app, db)
flask db init
flask db migrate -m "initial schema"
flask db upgrade

Every schema change is a migration. The migration has a down migration, so a bad change can be reversed. The migrations live in the repo, get code-reviewed, and ship with the code that needs them. That is the workflow that survives the team growing past three people.

The mistake to avoid: writing the schema change in a SQL file and running it manually against production. It works once. The second time, the manual change is on a different machine from the code that depends on it, and the deploy fails in a way nobody can reproduce.

The deploy-side checklist

For a Flask + MySQL deploy to actually work:

  1. MySQL is reachable from the deploy. On a PaaS, the database is in the same private network. On a VM, the firewall has to allow port 3306 from the app’s IP.
  2. The database is initialized. The first deploy runs the migrations. Subsequent deploys run only the new migrations.
  3. The credentials are set as environment variables. Not in the code, not in a committed file, not in the build cache.
  4. The connection pool is sized for the deploy’s concurrency. A 1-worker dev server with a 5-connection pool is fine. A 4-worker production deploy needs at least 20 connections.
  5. The MySQL server has a max_connections higher than the app’s worst-case pool size. 20 workers × 10 connections each = 200. max_connections=200 minimum.
  6. The health check actually pings the database. A /health that returns 200 without touching MySQL lies to the deploy platform, and the platform keeps routing traffic to a broken service.

The deploy platform should do most of this automatically. If it does not, the team is doing it by hand, and the next outage is the one where someone forgot.

The opinion this post is built on

The reason most “Flask MySQL” tutorials produce code that breaks in production is that they conflate the driver, the ORM, the connection pool, the migration tool, and the credential store. Each of those is a separate concern with a separate best practice. Conflating them produces code that works locally, ships to production, and falls apart the first time the deploy scales up.

The clean separation is:

  • PyMySQL for the driver. Pure Python, no compile step, every PaaS works.
  • Flask-SQLAlchemy for the session and the ORM. One connection pool, one session lifecycle, one declarative schema.
  • Flask-Migrate for schema changes. Every change is a migration in the repo, with a down migration and a code review.
  • Environment variables for credentials. Read at startup, never in code, never in a config file.
  • A platform secret store for the secret values. The env var is the bridge between the platform and the app.

A team that gets those five things right will not have a Flask-MySQL connection problem. A team that skips any of them will, and the failure mode will be specific to the thing they skipped.

For a deeper look at the deploy side, the RunxBuild platform is a good example of what a deploy layer that handles secret injection, connection pooling, and migrations cleanly looks like. The platform does not solve the schema design; the platform solves the part where the code, the credentials, and the runtime have to agree.

FAQ

Which MySQL driver should I use with Flask?

PyMySQL is the right default for new projects. It is pure Python, installs cleanly on every PaaS, and has no system dependencies. mysqlclient is faster on raw query throughput but requires a C compile step, which complicates slim Docker images. For 90% of workloads, the PyMySQL performance is fine. Swap to mysqlclient if a profile proves the driver is the bottleneck.

Do I need Flask-SQLAlchemy or can I use the MySQL driver directly?

Use Flask-SQLAlchemy. It gives you a session lifecycle, a connection pool, and a declarative ORM in one integration. Using the driver directly means reinventing those three things, usually worse. The only reason to skip SQLAlchemy is if you are writing a thin read-only script against a single table, in which case the driver is fine.

Where do I put the MySQL credentials?

In environment variables, read at startup. Never in app.config as a string literal, never in a config file that gets committed, never in a build argument. The platform’s secret store should be the source of truth; the env var is the bridge between the platform and the app.

Why is my Flask app getting MySQL server has gone away errors?

Almost always because the connection was idle, MySQL closed it, and the pool handed out a dead connection. Add pool_pre_ping=True to the engine options, set pool_recycle to less than MySQL’s wait_timeout (default 28800 seconds), and the problem goes away. If it persists, check that the database is on the same network and the firewall is not dropping long-lived TCP connections.

Should I use db.create_all() or migrations?

db.create_all() for the first deploy, Flask-Migrate for everything after. create_all() does not track schema changes, does not generate down migrations, and does not survive a second deploy to the same database. Migrations live in the repo, get code-reviewed, and ship with the code that needs them.

How many MySQL connections does my Flask app need?

Roughly the number of worker processes times the pool size. A 4-worker Gunicorn deploy with a 10-connection pool is up to 40 connections to MySQL. Set MySQL’s max_connections to at least that number, ideally 2x to leave headroom for migrations, admin connections, and other services. If you hit Too many connections, raise max_connections or lower the pool size.

#how to connect flask to mysql database#flask mysql#flask sqlalchemy#mysql connector python#pymysql flask#flask db connection