Prerequisites
- uv and Python 3.11 or later. This guide was tested with Python 3.13, FastAPI 0.142, SQLAlchemy 2.1, asyncpg 0.31, Alembic 1.20, and Uvicorn 0.54.
- A ClickHouse Cloud account
psql, to create the database. You can also run theCREATE DATABASEstatement in the SQL console.
Create a ClickHouse Managed Postgres service
In the ClickHouse Cloud console, click New service and select Postgres. The instance is ready in a few minutes. See the quickstart for a walkthrough.Get your connection details
Click Connect in the left sidebar of your service. The modal shows your username, password, server, and port, and it has a Directly / via PgBouncer toggle. This guide uses both connections:- PgBouncer (port
6432) for the app. SQLAlchemy keeps a pool of connections in every Uvicorn worker, and the bundled PgBouncer lets many workers and replicas share a small number of Postgres connections. It runs in transaction pooling mode. - Direct (port
5432) for Alembic. Migrations run DDL, can run for a long time, and may depend on session state, so they should talk to Postgres directly.
sslmode=verify-full&sslrootcert=.... asyncpg doesn’t read these parameters, so copy only the password and host from the modal. The app passes the CA certificate in code instead (see Configure the connection).
Set up the project
Create a project and install FastAPI, Uvicorn, SQLAlchemy with its asyncio extra, asyncpg, and Alembic:Configure the connection
Move the CA certificate you downloaded into the project directory and rename it toca-certificate.pem.
Create a database for the app. Replace <PASSWORD> and the host with the values from the Connect modal:
.env file in the project root, or add these variables to your existing one. The two URLs differ only in the port, use the postgresql+asyncpg scheme, and have no SSL parameters:
.env
DATABASE_CA_CERT is resolved relative to the directory you run commands from. If your password contains special characters such as @, /, or #, URL-encode them. Keep .env out of version control.
Create app/db.py. It builds the TLS settings, the engine, and a session factory that the rest of the app shares:
app/db.py
ssl.SSLContext created with PROTOCOL_TLS_CLIENT requires a valid certificate and checks the hostname, which is the equivalent of sslmode=verify-full. SQLAlchemy passes it to asyncpg through connect_args.
If you’d rather keep the TLS settings in the URL, or your app uses synchronous SQLAlchemy sessions, use the psycopg driver instead of asyncpg. It’s built on libpq and accepts the parameters from the Connect modal, for example
postgresql+psycopg://...:5432/guide_fastapi?sslmode=verify-full&sslrootcert=ca-certificate.pem, with both create_engine and create_async_engine.Define the model
Createapp/models.py with a typed SQLAlchemy model:
app/models.py
Run migrations
Create an Alembic environment from the async template:migrations/env.py. The new version loads your models for autogenerate and connects directly to Postgres with the same TLS settings as the app:
migrations/env.py
sqlalchemy.url in alembic.ini, because env.py builds the engine itself.
Generate a migration from the model, then apply it:
migrations/versions and commit it with your code. Alembic records the applied revision in the alembic_version table. Each time you change a model, run revision --autogenerate and upgrade head again.
Add the API endpoints
Createapp/main.py. Each request gets its own AsyncSession through a FastAPI dependency, and the engine’s pool is closed when the app shuts down:
app/main.py
session.commit. When the session closes, any uncommitted work is rolled back. On INSERT, SQLAlchemy reads the generated id and created_at back with RETURNING, so create_todo doesn’t need an extra query.
Run and verify
Start the app:DELETE request returns 204 No Content. Requesting a todo that doesn’t exist returns 404:
pg_stat_ssl with the same TLS settings as the app:
guide_fastapi and then public, and click the todos table. The alembic_version table next to it holds the migration history. The todos table contains the remaining row:
Stop the app with Ctrl+C.
Using PgBouncer
The app works through PgBouncer with the default asyncpg and SQLAlchemy settings:- asyncpg runs every query as a named, protocol-level prepared statement, and SQLAlchemy caches up to 100 of them per connection. The bundled PgBouncer supports these statements in transaction pooling mode, and asyncpg frees them with a protocol-level
Closemessage rather than SQLDEALLOCATE. You don’t needstatement_cache_size=0or a customprepared_statement_name_func. If you turn off asyncpg’s statement cache anyway, also addprepared_statement_cache_size=0to the URL to turn off SQLAlchemy’s cache. - Session settings made with
SET, such assearch_path, don’t carry over to the next transaction and can leak to other clients. UseSET LOCALinside a transaction instead. pg_stat_sslreports the connection between PgBouncer and Postgres, so the TLS check above returns(False, None)through port6432. The app’s connection to PgBouncer is still verified: a wrong CA certificate fails in the same way.- Alembic doesn’t take advisory locks and runs each
upgradein one transaction, so it also works through PgBouncer. The direct connection is still the better choice for long migrations.
Next steps
- Connection: connection strings, PgBouncer, and TLS
- Settings: Postgres and PgBouncer parameters, such as
max_connections - Read replicas: send read-only queries to a replica with a second engine
- Security: IP access lists and private networking