Replit quietly made a lot of database tutorials less weird: as of December 2025, Replit is hosted on GCP. That means your "toy app" can sit pretty close to a real Cloud SQL MySQL instance. Less latency. Fewer network hops. Fewer excuses for why SELECT 1 takes half a second.
This post is the practical path: get a Replit Python app talking to a GCP Cloud SQL MySQL database without turning your project into a security-themed escape room.
Prereqs (don't skip these)
You need:
- A GCP project with billing enabled
- A Cloud SQL MySQL instance already running
- A database and a user created
- A Replit app (I'll show Python)
If you don't have the DB/user yet, go create them now. If you're tempted to use the root user, take a walk and think about your choices.
Here's the checklist version of "created":
-
Cloud SQL instance exists
In the Cloud Console: Cloud SQL - Instances - your instance. Confirm it's MySQL, and it's in a region you actually want. -
Database exists
Cloud SQL - your instance - Databases - Create database. Example:appdb. -
User exists
Cloud SQL - your instance - Users - Add user account. Example:appuser. Give it a real password. Don't reuse your Gmail password. Yes, I have to say this.
Smart move: give the user only the permissions your app needs. For a prototype, you might go broader. For anything real, tighten it.
If you prefer SQL over clicking:
CREATE DATABASE appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'appuser'@'%' IDENTIFIED BY 'a-long-password-here';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX
ON appdb.* TO 'appuser'@'%';
FLUSH PRIVILEGES;
That @'%' is convenient because it allows connections from anywhere. It's also why you need to be strict about network access.
Two ways to connect (pick your pain)
Option A: Public IP + Authorized Networks (fastest setup)
This is the "I want it working in 15 minutes" option.
Cloud SQL gets a public IP. Your app connects like it would to any MySQL server. You limit inbound access by whitelisting IPs in Authorized networks.
Steps:
-
In Cloud SQL, enable Public IP
Cloud SQL - your instance - Connections - Networking.
Make sure "Public IP" is enabled. -
Add an Authorized network (an allowed inbound IP/CIDR)
Same screen: "Authorized networks" - Add network.
You'll enter something like: 203.0.113.42/32(single IP)-
or a CIDR range if you really mean it
-
Use normal MySQL credentials from your app
Host = the instance public IP
Port = 3306
Username/password = the user you created
Database =appdb
For hobby apps and prototypes, this is fine. For production, open networks eventually bite you. Not "maybe." Eventually.
The annoying part: figuring out what IP your Replit app is actually coming from.
A few realities:
- Replit workloads may not have a stable outbound IP forever.
- Even if you find an IP today, it can change tomorrow. Then Cloud SQL blocks you. You get timeouts. You question your career.
If you're doing Option A, make debugging easy. Run this in Replit:
import urllib.request
print(urllib.request.urlopen("https://api.ipify.org").read().decode())
Take that IP and add it to Authorized networks as /32. If it changes later, you'll repeat the ritual.
Option A tradeoffs:
- Pros: simplest, fastest to set up, no proxy process
- Cons: IP whitelisting pain, public exposure, passwords still in play
Option B: Cloud SQL Auth Proxy (more secure)
This is the "I don't want to sprinkle database access across the internet" option.
The Cloud SQL Auth Proxy creates a local TCP listener (often 127.0.0.1:3306, or another port you choose). Your app connects to localhost. The proxy handles the secure connection to Cloud SQL using IAM credentials.
- You run the Cloud SQL Auth Proxy and connect to
localhost. - Auth can be handled via IAM/service identity instead of "passwords taped under the keyboard."
Passwords in config files are a slow-moving mess. Even if you swear you'll rotate them later. You won't. Then you'll be digging through git history looking for that password you "only used for a minute."
Cloud SQL Auth Proxy docs (shockingly decent):
https://cloud.google.com/sql/docs/mysql/sql-proxy
How this usually works on Replit:
Replit isn't a full VM you manage like GCE, so you have to decide:
- Where the proxy binary lives (download at runtime, or check it in-don't check in secrets)
- How you start it (a "Run" command that starts proxy + app, or a small supervisor script)
- How you provide credentials (service account JSON in Secrets, or workload identity if supported)
A pragmatic setup for many small apps:
- Store a service account JSON in Replit Secrets (as a single string)
- Write it to a file at runtime
- Start the proxy pointing at that file
- Connect your app to 127.0.0.1
Yes, that still involves a secret. It's still better than scattering DB passwords across configs and laptops.
Option B tradeoffs:
- Pros: tighter security model, no public IP whitelisting games, better long-term story
- Cons: more moving parts, you have to run a proxy process, you need IAM set up correctly
If strangers will use your app, Option B is where you should land.
Store credentials in Replit Secrets (not in code)
In Replit, go to Secrets and set:
DB_HOST= Cloud SQL public IP (Option A) or127.0.0.1(Option B)DB_PORT=3306(or whatever local port your proxy uses)DB_NAME=appdbDB_USER=appuserDB_PASS= your password (Option A, and sometimes Option B depending on how you auth)
Never store passwords in Git. Git is forever. Your future self will not thank you.
Another practical point: don't commit "almost secrets" either. Instance IPs, usernames, and database names aren't as sensitive as passwords, but they still make your setup easier to attack and harder to rotate. Secrets gives you one place to change things without a commit.
Python example (pymysql)
Install:
pip install pymysql
Then:
import os
import pymysql
from pymysql.cursors import DictCursor
def get_conn():
return pymysql.connect(
host=os.environ["DB_HOST"],
port=int(os.environ.get("DB_PORT", "3306")),
user=os.environ["DB_USER"],
password=os.environ["DB_PASS"],
database=os.environ["DB_NAME"],
cursorclass=DictCursor,
connect_timeout=10,
read_timeout=10,
write_timeout=10,
autocommit=True,
charset="utf8mb4",
)
def main():
conn = get_conn()
try:
with conn.cursor() as cur:
cur.execute("SELECT NOW() AS server_time;")
row = cur.fetchone()
print(row)
finally:
conn.close()
if __name__ == "__main__":
main()
That SELECT NOW() test is boring. Good. Boring is stable.
A more realistic example (create table + insert + query)
This is where people find out they forgot autocommit=True, or they're using the wrong database, or their user has no permissions.
import os
import pymysql
from pymysql.cursors import DictCursor
def conn():
return pymysql.connect(
host=os.environ["DB_HOST"],
port=int(os.environ.get("DB_PORT", "3306")),
user=os.environ["DB_USER"],
password=os.environ["DB_PASS"],
database=os.environ["DB_NAME"],
cursorclass=DictCursor,
autocommit=True,
charset="utf8mb4",
)
with conn() as c:
with c.cursor() as cur:
cur.execute("""
CREATE TABLE IF NOT EXISTS notes (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
body TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)
""")
cur.execute("INSERT INTO notes (body) VALUES (%s)", ("hello from replit",))
cur.execute("SELECT id, body, created_at FROM notes ORDER BY id DESC LIMIT 3")
print(cur.fetchall())
Use parameterized queries (%s) like this. Don't build SQL strings with f-strings unless you enjoy SQL injection.
Connection pooling (do you need it?)
For tiny apps, opening a connection per request is fine. For busier apps, it gets slow and noisy. Cloud SQL also has connection limits, and you can hit them faster than you think.
In Python, you can use a pool (or at least reuse a global connection carefully). If you're using a web framework (Flask/FastAPI), a decent middle ground is:
- create a connection per request
- close it at the end of the request
- add pooling later if you actually need it
Pooling adds complexity. It also fixes real problems. Add it when you see connection churn and latency, not as a hobby.
Troubleshooting (the usual suspects)
"Can't connect"
This is almost always:
- Wrong host/user/pass
- Cloud SQL not allowing your inbound IP (Option A)
- You're in the wrong region or hitting a private IP you can't route to
- SSL/TLS requirements (less common, but it happens)
- Proxy not running (Option B)
Checks that actually save time:
-
Confirm the instance IP you're using
Cloud SQL - instance - Overview - Public IP address. Copy/paste it. Don't trust your memory. -
Confirm the user exists and can access the database
Try connecting from Cloud Shell or your laptop using the same credentials. If you can't connect from anywhere, Replit won't magically fix it. -
Authorized networks matches your outbound IP (Option A)
If you're using Authorized Networks, confirm the IP Replit uses. If it changed, Cloud SQL will block you and you'll stare at timeouts like it's a personality test. -
Proxy is listening locally (Option B)
If your app connects to127.0.0.1:3306, but nothing is listening there, you'll get a fast "connection refused," not a slow timeout. That difference matters.
Slow queries
Put Cloud SQL in the same region as your app. Since Replit is on GCP now, this actually matters. Cross-region latency is real, and it stacks up fast if your app does multiple queries per page load.
Then do the unsexy work:
- add indexes (for the columns you filter/sort on)
- stop doing
SELECT * - don't sort giant tables in Python
- don't do N+1 queries (fetching a list, then querying once per item)
Example: if you have a users table and you frequently look up by email, add an index:
CREATE INDEX idx_users_email ON users(email);
If you're sorting by created_at and filtering by user_id, a compound index often helps:
CREATE INDEX idx_notes_user_created ON notes(user_id, created_at);
Indexes speed up reads and slow down writes a bit. For most app tables, it's worth it.
"I hardcoded the password... just for now"
No. Use Secrets. "Just for now" turns into "six months later" with impressive consistency.
Also: if you already committed it once, deleting it from the file doesn't delete it from Git history. Rotate the password. Yes, even if it's annoying. Especially if it's annoying.
Extra guides you might want next
- Cloud SQL Auth Proxy docs: https://cloud.google.com/sql/docs/mysql/sql-proxy
- If you're doing this with other engines, I've got similar guides for AWS RDS MySQL, AWS RDS MariaDB, and GCP PostgreSQL (same idea, different flavor of networking pain).
If you get stuck, assume it's networking or credentials first. I've used this setup on a few projects, and the failures are usually boring. MySQL is usually innocent. Not always, but usually.
-Sethers