Cloud Functions can connect to Cloud SQL, but they do it in a way that’s inefficient by default, leading to performance issues and connection exhaustion.

Here’s a typical scenario:

from google.cloud import sql_v1beta4
from google.cloud.sql.connector import Connector, IPTypes
import sqlalchemy

# Initialize the SQL Admin API client
sqladmin = sql_v1beta4.SqlInstancesServiceClient()

# Replace with your instance connection name
instance_connection_name = "your-project:your-region:your-instance"

# Initialize the Connector
connector = Connector()

def get_connector():
    return connector.connect(
        instance_connection_name,
        "pg8000", # Or "pymysql" for MySQL
        ip_type=IPTypes.PUBLIC,
    )

# Create a SQLAlchemy connection pool
pool = sqlalchemy.create_engine(
    "postgresql+pg8000://",
    creator=get_connector,
)

def hello_world(request):
    """Responds to any HTTP request.
    Args:
        request (flask.Request): HTTP request object.
    Returns:
        The JSON response text, or any error.
    """
    try:
        with pool.connect() as db_conn:
            # Example query
            results = db_conn.execute(sqlalchemy.text("SELECT NOW()")).fetchone()
            return f"Hello, World! Current time is: {results[0]}"
    except Exception as e:
        return f"An error occurred: {e}", 500

This looks fine at first glance, but let’s trace what happens on each invocation.

When hello_world is called, get_connector() is invoked. This function always establishes a brand new, direct TCP connection to your Cloud SQL instance using the Cloud SQL Auth Proxy logic embedded within the Connector. It doesn’t check if there’s an existing, idle connection available.

Then, sqlalchemy.create_engine is configured with creator=get_connector. When pool.connect() is called, it uses this creator function to get a connection. Since get_connector always creates a new connection, the sqlalchemy pool ends up being a pool of connections that are all created on-demand, one per function invocation.

This means that for every single request to your Cloud Function, a new secure TCP connection is established. Cloud SQL instances have a finite limit on the number of concurrent connections (e.g., 500 for a basic tier MySQL, more for higher tiers). If your Cloud Functions are invoked frequently, especially concurrently, you’ll quickly hit this limit. Cloud SQL will start rejecting new connections, and your Cloud Functions will see errors like Too many connections or Connection refused.

The core problem is that the Connector is designed to provide secure, authenticated connections, but it doesn’t inherently manage a persistent pool of these connections across function invocations. When used with a standard sqlalchemy.create_engine, it effectively bypasses sqlalchemy’s connection pooling capabilities by always being asked to create a new connection.

The fix involves keeping the Connector and its underlying connection logic alive and reusing connections across invocations.

1. Initialize the Connector at the Module Level:

# ... (imports and instance_connection_name)

# Initialize the Connector once
connector = Connector()

def get_connector_with_pool(instance_connection_name, db_type):
    # This function will be called by SQLAlchemy to get a connection.
    # The connector object is reused, and SQLAlchemy manages the pool.
    conn = connector.connect(
        instance_connection_name,
        db_type,
        ip_type=IPTypes.PUBLIC,
    )
    return conn

The key here is that connector = Connector() is now initialized outside of the function handler. This means the connector object persists between invocations of your Cloud Function.

2. Configure SQLAlchemy to use the persistent Connector:

import sqlalchemy
from google.cloud.sql.connector import Connector, IPTypes
from google.cloud import sql_v1beta4 # Only needed for instance name if not hardcoded

# Initialize the Connector once at the module level
connector = Connector()

# Replace with your instance connection name
instance_connection_name = "your-project:your-region:your-instance"
db_type = "pg8000" # or "pymysql"

# Function to get a connection using the *persistent* connector
def get_db_conn():
    conn = connector.connect(
        instance_connection_name,
        db_type,
        ip_type=IPTypes.PUBLIC,
    )
    return conn

# Configure SQLAlchemy's connection pool
# SQLAlchemy will call get_db_conn() to get a connection when it needs one.
# The 'pool_size' and 'max_overflow' control how many connections are kept open.
# Adjust these based on your expected load and Cloud SQL instance limits.
pool = sqlalchemy.create_engine(
    f"postgresql+pg8000://", # Or "mysql+pymysql://"
    creator=get_db_conn,
    pool_size=5,     # Keep 5 connections open
    max_overflow=10, # Allow up to 10 additional connections on demand
    pool_timeout=30, # Wait up to 30 seconds for a connection
)

def hello_world(request):
    """Responds to any HTTP request.
    Args:
        request (flask.Request): HTTP request object.
    Returns:
        The JSON response text, or any error.
    """
    try:
        with pool.connect() as db_conn: # This now uses the connection pool
            results = db_conn.execute(sqlalchemy.text("SELECT NOW()")).fetchone()
            return f"Hello, World! Current time is: {results[0]}"
    except Exception as e:
        return f"An error occurred: {e}", 500

By initializing connector at the module level and passing creator=get_db_conn to sqlalchemy.create_engine, you’re telling SQLAlchemy to manage a pool of connections, and each time it needs a new connection, it calls get_db_conn. Because connector is global, get_db_conn reuses the underlying connection logic. SQLAlchemy’s pooling mechanism then takes over, reusing connections from the pool rather than creating a new one for every single invocation.

The pool_size argument to create_engine is crucial. It tells SQLAlchemy how many connections to keep open and ready in the pool. A pool_size of 5 means that even if your function is idle for a while, there will be up to 5 connections already established and waiting for requests. max_overflow allows the pool to temporarily exceed pool_size if there’s a spike in demand.

The most surprising thing about this setup is that the google-cloud-sql-connector library itself doesn’t expose a direct pooling API for the connections it creates. It’s designed to be a drop-in replacement for the underlying database driver’s connection function, and it relies on the caller (in this case, SQLAlchemy) to manage the pooling.

You will likely encounter a new error related to connection limits if you set pool_size too high for your Cloud SQL instance tier, or if you have many Cloud Functions instances scaling up and each using a connection from the pool simultaneously. The next step would be to tune pool_size and max_overflow based on your Cloud SQL instance’s max_connections setting and your expected Cloud Functions concurrency.

Want structured learning?

Take the full Cloud-functions course →