Connection Pooling di Distributed System
15 min read

Connection Pooling di Distributed System

Connection pooling adalah konsep yang dipahami dengan baik di dunia monolith: buat pool saat startup, pinjam koneksi saat ada request, kembalikan ke pool setelah selesai, atur batas maksimumnya, dan sistem berjalan stabil. Masalah muncul ketika pemahaman yang sama dibawa masuk ke distributed system tanpa pertanyaan. Di sana, hampir semua asumsi yang mendasari model klasik itu runtuh — dan runtuhnya bukan perlahan, tapi tiba-tiba, di production, saat traffic naik. Artikel ini membahas mengapa model klasik gagal secara mendasar di distributed system, bagaimana connection exhaustion terjadi di Lambda, dan bagaimana RDS Proxy — serta alternatif lainnya — menggeser tanggung jawab pooling ke lapisan yang tepat.

Asumsi yang Runtuh di Distributed System

Connection pool klasik bekerja karena satu asumsi mendasar yang hampir tidak pernah disebutkan secara eksplisit: jumlah instance aplikasi yang mengakses database adalah angka yang kecil dan terprediksi.

Di monolith dengan 3 instance di belakang load balancer, kamu bisa menghitung dengan tepat berapa koneksi maksimum yang akan dibuka ke database: 3 instance × 20 koneksi per pool = 60 koneksi. Angka ini tidak akan berubah kecuali kamu menambah instance secara manual. Ini membuat konfigurasi max_connections di PostgreSQL atau MySQL menjadi mudah dan deterministik.

Di distributed system, terutama yang serverless, semua angka ini berubah menjadi variabel yang bisa berfluktuasi dalam hitungan detik.

flowchart TD
    subgraph Monolith["Monolith — Terprediksi"]
        APP1[Instance 1\nPool: 20 conn] --> DB1[(Database\nmax_conn: 100)]
        APP2[Instance 2\nPool: 20 conn] --> DB1
        APP3[Instance 3\nPool: 20 conn] --> DB1
        NOTE1["Total koneksi: 60\nDeterministik, aman"]
    end

    subgraph Serverless["Lambda — Tidak Terprediksi"]
        L1[Lambda #1\nPool: 5 conn] --> DB2[(Database\nmax_conn: 100)]
        L2[Lambda #2\nPool: 5 conn] --> DB2
        L3[Lambda #3\nPool: 5 conn] --> DB2
        LDOT["... hingga\n500 instance"] --> DB2
        NOTE2["Total koneksi: bisa 2.500\nConnection exhaustion!"]
    end

    style NOTE2 fill:#ffebee,stroke:#e53935
    style NOTE1 fill:#e8f5e9,stroke:#43a047

Ini bukan skenario edge case. Ini adalah perilaku normal Lambda saat menghadapi traffic spike — dan inilah mengapa connection pooling di level aplikasi tidak cukup di arsitektur serverless.


Anatomi Connection Exhaustion di Lambda

Untuk memahami masalahnya dengan tepat, perlu dipahami dulu bagaimana Lambda mengelola instance dan koneksi database.

Lifecycle Lambda Instance

Lambda tidak menjalankan semua invocation di satu proses. Setiap concurrent execution membutuhkan instance tersendiri, dan setiap instance memiliki runtime yang sepenuhnya terisolasi — termasuk memory, variabel global, dan koneksi database.

stateDiagram-v2
    [*] --> ColdStart: Invocation pertama\natau kapasitas habis
    ColdStart --> WarmInit: Container dibuat\nRuntime diinisialisasi
    WarmInit --> Running: Handler dieksekusi\nKoneksi DB dibuka
    Running --> Idle: Handler selesai\nKoneksi tetap terbuka di pool
    Idle --> Running: Invocation berikutnya\n(warm start, reuse koneksi)
    Idle --> Terminated: Idle terlalu lama\natau Lambda reclaim

    note right of Running: Setiap concurrent execution\nmembutuhkan instance berbeda
    note right of Idle: Koneksi di pool tetap hidup\nselama instance idle

Yang krusial dari diagram ini: koneksi database dibuka saat cold start dan tetap terbuka selama instance masih hidup — bahkan saat tidak ada handler yang berjalan. Ini adalah perilaku yang diinginkan di monolith (reuse koneksi = efisien), tapi di Lambda ia menciptakan masalah: ratusan instance idle yang masing-masing memegang koneksi ke database.

Kronologi Connection Exhaustion

Berikut skenario yang sangat umum terjadi di production:

sequenceDiagram
    participant Traffic as Traffic (spike)
    participant Lambda as Lambda Instances
    participant DB as PostgreSQL (max 100 conn)

    Note over DB: max_connections = 100

    Traffic->>Lambda: 10 concurrent requests
    Lambda->>DB: 10 instance × 3 conn = 30 koneksi
    Note over DB: 30/100 koneksi terpakai — aman

    Traffic->>Lambda: Traffic naik 5x
    Lambda->>DB: 50 instance × 3 conn = 150 koneksi
    Note over DB: 100/100 koneksi — database penuh!

    Lambda->>DB: Instance #51 mencoba connect
    DB-->>Lambda: FATAL: sorry, too many clients already
    Lambda-->>Traffic: HTTP 500 — Lambda timeout

    Note over Lambda,DB: Instance baru terus dibuat\nTapi tidak bisa connect\nSemua request gagal

Yang membuat situasi ini bertambah buruk: Lambda yang gagal connect biasanya langsung retry, yang justru menambah tekanan ke database yang sudah kewalahan. Ini adalah positive feedback loop menuju kegagalan total.

Mengapa Pool di Level Kode Tidak Membantu

Reaksi pertama banyak engineer ketika menghadapi masalah ini adalah mengurangi MaxOpenConns di level kode. Ini memang mengurangi koneksi per instance — tapi tidak menyelesaikan masalah fundamentalnya.

// ANTI-PATTERN: menganggap ini cukup untuk Lambda
sqlDB.SetMaxOpenConns(5) // 5 koneksi per instance
// Dengan 200 concurrent Lambda: 200 × 5 = 1000 koneksi ke DB
// Database tetap exhausted

// Setengah solusi: kurangi drastis
sqlDB.SetMaxOpenConns(1) // 1 koneksi per instance
// Dengan 200 concurrent Lambda: masih 200 koneksi ke DB
// Lebih baik, tapi tetap tidak terkontrol dan tidak bisa di-cap

Tidak ada nilai MaxOpenConns di level Lambda yang bisa memberikan batas atas yang pasti untuk koneksi ke database — karena kamu tidak bisa mengontrol berapa banyak Lambda instance yang dibuat oleh AWS.

Mengurangi MaxOpenConns di Lambda adalah optimasi yang benar tapi tidak mencukupi. Ia hanya mengurangi keparahan masalah, bukan menghilangkannya. Selama Lambda bisa scale ke ratusan instance, tidak ada setting pool di level kode yang bisa memberikan perlindungan yang sesungguhnya terhadap connection exhaustion.

RDS Proxy: Menggeser Pooling ke Infrastruktur

Solusi yang tepat bukan memperbaiki pooling di level aplikasi, tapi memindahkan pooling ke lapisan yang berada di antara aplikasi dan database — lapisan yang bisa melihat dan mengontrol semua koneksi secara terpusat, terlepas dari berapa banyak instance aplikasi yang ada.

RDS Proxy adalah implementasi resmi AWS untuk pola ini.

Cara Kerja RDS Proxy

sequenceDiagram
    participant L1 as Lambda Instance 1
    participant L2 as Lambda Instance 2
    participant L3 as Lambda Instance N
    participant PROXY as RDS Proxy\n(Shared Pool)
    participant RDS as RDS Database

    L1->>PROXY: Connect (koneksi ringan)
    L2->>PROXY: Connect (koneksi ringan)
    L3->>PROXY: Connect (koneksi ringan)

    Note over PROXY: Proxy memiliki pool koneksi\nke RDS yang terpusat dan terbatas

    PROXY->>RDS: Hanya membuka koneksi\nsesuai yang dibutuhkan\n(bukan per Lambda instance)

    L1->>PROXY: Query: SELECT * FROM orders
    PROXY->>RDS: Forward via koneksi yang sudah ada di pool
    RDS-->>PROXY: Result
    PROXY-->>L1: Result

    L1->>PROXY: Query selesai, koneksi dikembalikan ke pool
    Note over PROXY: Koneksi dikembalikan ke pool\nsiap dipakai Lambda lain
    L2->>PROXY: Query berikutnya menggunakan koneksi yang sama

Perbedaan fundamentalnya: tanpa proxy, setiap Lambda instance mempertahankan koneksinya sendiri ke database. Dengan proxy, semua Lambda instance terhubung ke proxy (koneksi yang sangat ringan), dan proxy yang mengelola koneksi ke database secara terpusat.

Apa yang RDS Proxy Berikan

Selain pooling terpusat, RDS Proxy memberikan beberapa manfaat lain yang relevan untuk arsitektur Lambda:

Connection multiplexing. Satu koneksi fisik di pool proxy bisa digunakan bergantian oleh banyak Lambda instance yang tidak sedang aktif querying secara bersamaan. Ini adalah multiplexing yang tidak mungkin dilakukan di level aplikasi.

Failover yang lebih cepat. Saat terjadi failover RDS (primary down, replica dipromosikan), proxy meng-handle reconnection secara transparan. Lambda tidak perlu tahu bahwa failover terjadi.

Integrasi IAM dan Secrets Manager. RDS Proxy mendukung autentikasi menggunakan IAM token, sehingga Lambda tidak perlu menyimpan credential database — credential dikelola oleh Secrets Manager dan dirotasi secara otomatis.

Pinning control. RDS Proxy secara default melakukan connection pinning ketika mendeteksi operasi yang tidak bisa di-multiplex (seperti SET statement atau stored procedure yang mengubah state sesi). Memahami kapan pinning terjadi penting untuk memaksimalkan efisiensi proxy.


Setup RDS Proxy: Langkah per Langkah

Prasyarat Infrastruktur

Sebelum membuat RDS Proxy, pastikan komponen berikut sudah ada:

Prasyarat:
  ✓ RDS instance (PostgreSQL / MySQL) yang sudah berjalan
  ✓ AWS Secrets Manager secret yang menyimpan credential database
  ✓ VPC yang sama untuk Lambda, RDS Proxy, dan RDS
  ✓ Security group yang dikonfigurasi dengan benar
  ✓ IAM role untuk RDS Proxy (akses Secrets Manager)

Menyimpan Credential di Secrets Manager

RDS Proxy mengambil credential database dari Secrets Manager. Ini bukan hanya untuk keamanan — ini adalah requirement arsitektur proxy untuk bisa mengelola autentikasi secara terpusat.

# Buat secret untuk credential database
aws secretsmanager create-secret \
    --name "prod/myapp/rds-credentials" \
    --description "RDS credentials for myapp production" \
    --secret-string '{
        "username": "app_user",
        "password": "strong-password-here",
        "engine": "postgres",
        "host": "mydb.cluster-xyz.ap-southeast-1.rds.amazonaws.com",
        "port": 5432,
        "dbname": "app_db"
    }' \
    --region ap-southeast-1

# Output: catat SecretARN untuk digunakan saat buat proxy

Membuat IAM Role untuk RDS Proxy

Proxy perlu permission untuk membaca secret dari Secrets Manager.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowGetSecretValue",
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret"
      ],
      "Resource": "arn:aws:secretsmanager:ap-southeast-1:123456789:secret:prod/myapp/rds-credentials-*"
    },
    {
      "Sid": "AllowDecryptWithKMS",
      "Effect": "Allow",
      "Action": "kms:Decrypt",
      "Resource": "arn:aws:kms:ap-southeast-1:123456789:key/your-kms-key-id",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "secretsmanager.ap-southeast-1.amazonaws.com"
        }
      }
    }
  ]
}

Membuat RDS Proxy via AWS CLI

# Buat RDS Proxy
aws rds create-db-proxy \
    --db-proxy-name "myapp-prod-proxy" \
    --engine-family POSTGRESQL \
    --auth '[{
        "AuthScheme": "SECRETS",
        "SecretArn": "arn:aws:secretsmanager:ap-southeast-1:123456789:secret:prod/myapp/rds-credentials-AbCdEf",
        "IAMAuth": "DISABLED"
    }]' \
    --role-arn "arn:aws:iam::123456789:role/rds-proxy-role" \
    --vpc-subnet-ids subnet-aaa subnet-bbb subnet-ccc \
    --vpc-security-group-ids sg-proxy-id \
    --require-tls \
    --idle-client-timeout 1800 \
    --region ap-southeast-1

# Daftarkan RDS instance sebagai target proxy
aws rds register-db-proxy-targets \
    --db-proxy-name "myapp-prod-proxy" \
    --db-cluster-identifiers "myapp-prod-cluster" \
    --region ap-southeast-1

# Konfigurasi connection pool di target group
aws rds modify-db-proxy-target-group \
    --db-proxy-name "myapp-prod-proxy" \
    --target-group-name "default" \
    --connection-pool-config '{
        "MaxConnectionsPercent": 80,
        "MaxIdleConnectionsPercent": 50,
        "ConnectionBorrowTimeout": 120,
        "SessionPinningFilters": ["EXCLUDE_VARIABLE_SETS"]
    }' \
    --region ap-southeast-1

Parameter MaxConnectionsPercent: 80 berarti proxy akan menggunakan maksimal 80% dari max_connections database. Sisanya 20% adalah buffer untuk koneksi administratif dan monitoring. SessionPinningFilters: EXCLUDE_VARIABLE_SETS menginstruksikan proxy untuk tidak melakukan pinning saat ada SET statement — ini meningkatkan efisiensi multiplexing untuk kebanyakan aplikasi.

Konfigurasi Security Group

# Security group untuk RDS Proxy
# Inbound: dari Lambda security group, port database
aws ec2 authorize-security-group-ingress \
    --group-id sg-proxy-id \
    --protocol tcp \
    --port 5432 \
    --source-group sg-lambda-id \
    --region ap-southeast-1

# Security group untuk RDS
# Inbound: dari RDS Proxy security group saja (bukan langsung dari Lambda)
aws ec2 authorize-security-group-ingress \
    --group-id sg-rds-id \
    --protocol tcp \
    --port 5432 \
    --source-group sg-proxy-id \
    --region ap-southeast-1

# BENAR: Lambda → Proxy → RDS
# ANTI-PATTERN: Lambda → RDS langsung (bypass proxy, tidak ada pooling terpusat)

Implementasi Go: Lambda dengan RDS Proxy

Inisialisasi Koneksi yang Tepat

Di Lambda, ada satu trik penting: inisialisasi koneksi database di luar handler function. Ini memungkinkan koneksi di-reuse antar invocation pada instance yang sama (warm start).

package main

import (
    "context"
    "database/sql"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/aws/aws-lambda-go/lambda"
    _ "github.com/lib/pq"
)

// db dideklarasikan di level package — diinisialisasi sekali, di-reuse antar invocation
var db *sql.DB

func init() {
    // init() dipanggil sekali saat Lambda instance pertama kali dibuat (cold start)
    // Koneksi yang dibuat di sini akan di-reuse oleh semua invocation di instance yang sama
    var err error
    db, err = initDB()
    if err != nil {
        // Jika DB tidak bisa diinisialisasi, Lambda tidak bisa melayani request apapun
        // Lebih baik fail fast daripada return error di setiap invocation
        log.Fatalf("Gagal inisialisasi koneksi database: %v", err)
    }
}

func initDB() (*sql.DB, error) {
    // Gunakan endpoint RDS Proxy, bukan RDS langsung
    host := os.Getenv("DB_PROXY_ENDPOINT") // dari env var Lambda
    port := os.Getenv("DB_PORT")
    user := os.Getenv("DB_USER")
    password := os.Getenv("DB_PASSWORD")
    dbname := os.Getenv("DB_NAME")

    dsn := fmt.Sprintf(
        "host=%s port=%s user=%s password=%s dbname=%s sslmode=require",
        host, port, user, password, dbname,
    )

    database, err := sql.Open("postgres", dsn)
    if err != nil {
        return nil, fmt.Errorf("sql.Open gagal: %w", err)
    }

    // BENAR: pool di Lambda harus kecil — RDS Proxy yang jadi pengatur utama
    // MaxOpenConns kecil karena RDS Proxy yang melakukan multiplexing
    database.SetMaxOpenConns(2)
    database.SetMaxIdleConns(1)
    // ConnMaxLifetime pendek untuk memastikan koneksi di-rotate secara reguler
    // dan tidak menjadi stale setelah Lambda instance idle lama
    database.SetConnMaxLifetime(5 * time.Minute)
    // ConnMaxIdleTime: tutup koneksi idle yang tidak dipakai
    // Ini penting agar proxy bisa reclaim koneksi ke pool-nya
    database.SetConnMaxIdleTime(1 * time.Minute)

    // Verifikasi koneksi bisa dibuat
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    if err := database.PingContext(ctx); err != nil {
        return nil, fmt.Errorf("database ping gagal: %w", err)
    }

    log.Printf("Koneksi database berhasil ke: %s", host)
    return database, nil
}

// ANTI-PATTERN: inisialisasi koneksi di dalam handler
// func handler(ctx context.Context, event MyEvent) (string, error) {
//     db, _ := initDB()  // ✗ Buat koneksi baru setiap invocation
//     defer db.Close()   // ✗ Tutup koneksi setelah selesai — tidak ada reuse
//     // ...
// }

Handler dengan Context dan Retry

type OrderRequest struct {
    UserID  int64   `json:"user_id"`
    Amount  float64 `json:"amount"`
    Items   []Item  `json:"items"`
}

type OrderResponse struct {
    OrderID   int64  `json:"order_id"`
    Status    string `json:"status"`
}

func handler(ctx context.Context, req OrderRequest) (*OrderResponse, error) {
    // Gunakan context dari Lambda untuk propagasi cancellation dan timeout
    // Lambda secara otomatis cancel context ini saat function timeout
    order, err := createOrder(ctx, req)
    if err != nil {
        log.Printf("ERROR createOrder: %v", err)
        return nil, fmt.Errorf("gagal membuat order: %w", err)
    }

    return &OrderResponse{
        OrderID: order.ID,
        Status:  "created",
    }, nil
}

func createOrder(ctx context.Context, req OrderRequest) (*Order, error) {
    // Gunakan transaksi untuk operasi yang membutuhkan atomicity
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return nil, fmt.Errorf("BeginTx gagal: %w", err)
    }
    defer tx.Rollback() // No-op jika sudah Commit

    var orderID int64
    err = tx.QueryRowContext(ctx,
        "INSERT INTO orders (user_id, amount, status) VALUES ($1, $2, 'pending') RETURNING id",
        req.UserID, req.Amount,
    ).Scan(&orderID)
    if err != nil {
        return nil, fmt.Errorf("insert order gagal: %w", err)
    }

    for _, item := range req.Items {
        _, err = tx.ExecContext(ctx,
            "INSERT INTO order_items (order_id, product_id, quantity) VALUES ($1, $2, $3)",
            orderID, item.ProductID, item.Quantity,
        )
        if err != nil {
            return nil, fmt.Errorf("insert order_item gagal: %w", err)
        }
    }

    if err := tx.Commit(); err != nil {
        return nil, fmt.Errorf("Commit gagal: %w", err)
    }

    return &Order{ID: orderID}, nil
}

func main() {
    lambda.Start(handler)
}

Menggunakan IAM Authentication (Opsi yang Lebih Aman)

Daripada menyimpan password database di environment variable, Lambda bisa menggunakan IAM token untuk autentikasi ke RDS Proxy. Token ini di-generate secara otomatis dan berlaku 15 menit.

import (
    "context"
    "fmt"

    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/feature/rds/auth"
)

func generateIAMToken(ctx context.Context, proxyEndpoint, region, dbUser string) (string, error) {
    cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region))
    if err != nil {
        return "", fmt.Errorf("gagal load AWS config: %w", err)
    }

    // GenerateAuthToken membuat IAM authentication token yang berlaku 15 menit
    authToken, err := auth.BuildAuthToken(
        ctx,
        fmt.Sprintf("%s:5432", proxyEndpoint),
        region,
        dbUser,
        cfg.Credentials,
    )
    if err != nil {
        return "", fmt.Errorf("gagal generate IAM token: %w", err)
    }

    return authToken, nil
}

// Dengan IAM auth, Lambda tidak perlu tahu password database sama sekali
// IAM Policy Lambda hanya perlu: rds-db:connect ke resource proxy yang spesifik

IAM authentication memerlukan IAM policy yang memberikan permission rds-db:connect ke Lambda execution role, dan RDS Proxy dikonfigurasi dengan IAMAuth: REQUIRED. Pendekatan ini lebih aman karena tidak ada credential statis yang perlu dirotasi secara manual.


Pola untuk Arsitektur Lain

RDS Proxy adalah solusi spesifik untuk Lambda + RDS di AWS. Distributed system yang lebih umum punya beberapa pola lain yang menyelesaikan masalah serupa.

PgBouncer untuk PostgreSQL Self-hosted atau ECS

Jika infrastrukturmu tidak sepenuhnya di Lambda atau kamu butuh lebih banyak kontrol, PgBouncer adalah connection pooler open-source yang sangat battle-tested untuk PostgreSQL.

flowchart LR
    subgraph Services["Banyak Service Instance"]
        S1[Service A\nInstance 1]
        S2[Service A\nInstance 2]
        S3[Service B]
        S4[Service C]
    end

    subgraph Pooler["PgBouncer"]
        PG["PgBouncer\nTransaction pooling mode\nPool size: 50"]
    end

    subgraph DB["Database"]
        RDS[(PostgreSQL\nmax_connections: 100)]
    end

    S1 & S2 & S3 & S4 --> PG
    PG --> RDS

    note["Transaction mode:\nkoneksi dikembalikan ke pool\nsetelah setiap transaksi selesai\nbukan setelah sesi berakhir"]

PgBouncer mendukung tiga mode pooling:

Session mode — koneksi diassign ke client selama sesi berlangsung. Paling kompatibel dengan semua fitur PostgreSQL tapi paling sedikit multiplexing.

Transaction mode — koneksi diassign ke client hanya selama transaksi aktif, lalu dikembalikan ke pool. Ini adalah mode yang paling efisien dan cocok untuk kebanyakan aplikasi web.

Statement mode — koneksi diassign per statement. Paling agresif tapi tidak kompatibel dengan transaksi multi-statement.

Untuk microservice di ECS atau Kubernetes, deploy PgBouncer sebagai sidecar container atau sebagai service tersendiri.

ProxySQL untuk MySQL

Ekuivalen PgBouncer untuk MySQL adalah ProxySQL — proxy yang lebih feature-rich dengan kemampuan query routing, read/write splitting, dan monitoring yang lebih detail.

flowchart LR
    subgraph Apps["Microservices"]
        A1[Service 1]
        A2[Service 2]
        A3[Service 3]
    end

    subgraph ProxySQL["ProxySQL"]
        PS["ProxySQL\nQuery routing\nConnection pooling\nRead/Write splitting"]
    end

    subgraph MySQL["MySQL Cluster"]
        PRI[(Primary\nWrite)]
        REP1[(Replica 1\nRead)]
        REP2[(Replica 2\nRead)]
    end

    Apps --> PS
    PS -->|Write queries| PRI
    PS -->|Read queries| REP1
    PS -->|Read queries| REP2

ProxySQL dapat secara otomatis merutekan query berdasarkan apakah ia SELECT (ke replica) atau INSERT/UPDATE/DELETE (ke primary), tanpa perubahan di level aplikasi.


Observability: Apa yang Harus Dipantau

Pooling yang dikonfigurasi dengan benar tanpa observability yang memadai tetap berbahaya — kamu tidak akan tahu kapan mendekati batas atau kapan ada masalah yang berkembang.

Metrik yang harus dipantau di CloudWatch:

RDS:
  □ DatabaseConnections — total koneksi ke database
    Alert: > 80% dari max_connections
  □ FreeableMemory — memori yang tersedia
    Alert: < 20% dari total memory
  □ CPUUtilization
    Alert: > 80% selama > 5 menit

RDS Proxy:
  □ DatabaseConnectionRequests — request koneksi ke proxy
  □ DatabaseConnections — koneksi aktif dari proxy ke RDS
  □ DatabaseConnectionsCurrentlySessionPinned — pinned sessions
    (Tinggi = kemungkinan ada pattern yang bisa dioptimalkan)
  □ QueryDatabaseResponseTime — latensi response database
  □ ClientConnections — koneksi dari Lambda ke proxy

Lambda:
  □ Duration — jika naik drastis, mungkin ada connection wait
  □ Errors — spike errors bisa mengindikasikan connection failure
  □ Throttles — Lambda throttled karena concurrency limit
# Query metrik RDS connections via AWS CLI
aws cloudwatch get-metric-statistics \
    --namespace AWS/RDS \
    --metric-name DatabaseConnections \
    --dimensions Name=DBInstanceIdentifier,Value=myapp-prod \
    --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \
    --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
    --period 60 \
    --statistics Average Maximum \
    --region ap-southeast-1

# Query metrik RDS Proxy pinned sessions
aws cloudwatch get-metric-statistics \
    --namespace AWS/RDS \
    --metric-name DatabaseConnectionsCurrentlySessionPinned \
    --dimensions Name=ProxyName,Value=myapp-prod-proxy \
    --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \
    --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
    --period 60 \
    --statistics Average \
    --region ap-southeast-1
Jika metrik DatabaseConnectionsCurrentlySessionPinned konsisten tinggi, artinya proxy tidak bisa melakukan multiplexing secara optimal. Periksa apakah aplikasi menggunakan SET statement, prepared statement dengan cara yang menyebabkan pinning, atau transaksi yang sangat panjang. Setiap session yang pinned adalah koneksi fisik yang eksklusif — tidak bisa di-share.

Kapan Menggunakan Apa

GUNAKAN RDS Proxy jika:
  ✓ Lambda + RDS (ini adalah use case utamanya)
  ✓ Banyak short-lived service instance yang connect ke RDS
  ✓ Traffic sangat spiky dan tidak terprediksi
  ✓ Butuh failover yang transparan tanpa perubahan kode
  ✓ Sudah di ekosistem AWS dan ingin managed solution

GUNAKAN PgBouncer jika:
  ✓ PostgreSQL self-hosted atau di container (ECS, Kubernetes)
  ✓ Butuh kontrol penuh atas konfigurasi pooler
  ✓ Ingin menghindari biaya tambahan RDS Proxy
  ✓ Tim sudah familiar dengan administrasi PostgreSQL

GUNAKAN ProxySQL jika:
  ✓ MySQL dan butuh read/write splitting otomatis
  ✓ Butuh query routing berdasarkan pattern
  ✓ Butuh observability detail di level query

TIDAK BUTUH PROXY jika:
  ✗ Monolith dengan jumlah instance yang kecil dan terprediksi
  ✗ Service dengan pool yang sudah terkonfigurasi dengan benar
     dan jumlah instance yang di-cap
  ✗ CI/CD job atau batch processing yang tidak concurrent

Ringkasan

  • Pooling klasik gagal di distributed system karena asumsinya runtuh: jumlah instance tidak terprediksi, lifecycle pendek, dan tidak ada “global pool” yang bisa di-share antar instance.
  • Lambda + RDS tanpa proxy adalah bom waktu — saat traffic spike, ratusan instance masing-masing membuka koneksi dan database exhausted dalam hitungan detik.
  • Mengurangi MaxOpenConns di Lambda tidak cukup — ia mengurangi keparahan masalah tapi tidak menghilangkan ketidakpastian jumlah koneksi total karena bergantung pada berapa banyak instance yang dibuat AWS.
  • RDS Proxy menggeser pooling ke infrastruktur — satu pool terpusat yang melayani semua Lambda instance, dengan batas koneksi ke database yang bisa dikontrol secara eksplisit.
  • Inisialisasi koneksi di luar handler, bukan di dalam — ini memungkinkan reuse koneksi antar warm invocation dan menghemat cold start overhead.
  • Pool di Lambda harus kecilMaxOpenConns: 1–2 cukup karena RDS Proxy yang melakukan multiplexing. Pool besar di Lambda justru membuang potensi multiplexing proxy.
  • SessionPinningFilters: EXCLUDE_VARIABLE_SETS mengurangi pinning yang tidak perlu dan meningkatkan efisiensi multiplexing untuk kebanyakan aplikasi yang tidak memerlukan session-level state.
  • Pantau DatabaseConnectionsCurrentlySessionPinned — metrik ini menunjukkan seberapa efektif multiplexing berjalan. Nilai tinggi mengindikasikan potensi optimasi di level aplikasi.

Portofolio