Data API in Production

Ship a bursty Lambda fleet or spiky containers behind an API and traffic can jump from 50 RPS to 3,000 in seconds. Traditional database client pools melt under that, because thousands of short-lived workers fight over a finite number of database sockets - and nobody wants to babysit credentials on top of it. "Connectionless" patterns - HTTP-style database access or on-demand auth tokens - let you scale requests without multiplying connections or hoarding passwords. Here's how the two main paths work in production.

Two connectionless paths

RDS Data API - your code calls a regional HTTPS endpoint and AWS fans the query out to Aurora; there's no client TCP connection and no warm pool to manage. It fits spiky Lambda traffic, needs no driver, and authenticates through IAM. The cost is per-request latency plus payload, size, and throughput quotas - treat it like any external API, with retries, jitter, and idempotency keys.

IAM database authentication, optionally with RDS Proxy - instead of storing a password, the Lambda or container requests a short-lived SigV4 auth token with a 15-minute lifetime. Put RDS Proxy in front and thousands of workers reuse a controlled pool of backend connections, where you cap the maximum sockets and tune idle and percent limits. This keeps real drivers and full SQL features with central connection control; the watchouts are token-refresh cycles and a misconfigured proxy cap during a spike.

Request flow

Secrets path (preferred for passwords or app keys): the code requests the secret value from AWS Secrets Manager, caches it in memory, and uses it to authenticate or to sign a token call. As of 2025 GetSecretValue supports very high RPS, but you should still cache aggressively and respect regional quotas.

IAM auth path (no passwords): the code calls rds.generate_db_auth_token(...) to mint a 15-minute token, connects via RDS Proxy (recommended for Lambda) or directly to RDS, and re-mints before expiration or on a 1045/28P01 auth error.

Retry path: treat the Data API, Secrets Manager, and your own API Gateway as external services - exponential backoff with jitter and idempotency safeguards. Lambda's async invokes add their own retries (two by default) plus a long-lived internal queue, so know how those stack with yours to avoid a retry storm.

Comparing the two

ConcernData APIIAM Auth + RDS Proxy
Connection managementNone (HTTPS request)Centralized pool; you cap backends
CredentialsIAM onlyIAM token (15 min) or password in Secrets Manager
Cold startsMinimal client initKeep driver global; proxy hides DB handshakes
Throughput scalingBounded by API quotas; add retries/jitterBounded by proxy/backends; tune max and idle %
Dev ergonomicsNo driver, JSON in/outFull SQL and driver features
GotchasPayload size, statement timeouts, per-account throttlingToken rotation, idle timeouts, mis-sized pools

Lambda with RDS Data API

A minimal client owns its own backoff because the Data API is a regional HTTPS service with its own throttling - don't rely on the default SDK retries alone:

import os, time, random, boto3
from botocore.config import Config

rds = boto3.client("rds-data", config=Config(retries={"max_attempts": 0}))  # we handle retries
ARN_DB, ARN_SEC, DB_NAME = os.environ["DB_ARN"], os.environ["SECRET_ARN"], os.environ["DB_NAME"]

def _sleep_backoff(attempt):
    base = min(0.25 * (2 ** attempt), 5.0)
    time.sleep(random.uniform(0, base))           # full jitter

def exec_sql(sql, params=None, max_tries=6):
    for attempt in range(max_tries):
        try:
            return rds.execute_statement(resourceArn=ARN_DB, secretArn=ARN_SEC,
                                         database=DB_NAME, sql=sql, parameters=params or [])
        except rds.exceptions.ThrottlingException:
            _sleep_backoff(attempt)                # back off + jitter, like any HTTP service
        except rds.exceptions.BadRequestException as e:
            raise RuntimeError(f"SQL error: {e}")  # non-retryable
    raise TimeoutError("RDS Data API throttled too long")

Containers / Lambda with IAM token + RDS Proxy

Refresh the token before the 15-minute deadline, keep the connection global (outside the handler) to minimize cold-start cost, and let RDS Proxy cap backend sockets:

import os, time, pymysql, boto3

region, host = os.environ["AWS_REGION"], os.environ["DB_ENDPOINT"]   # RDS Proxy endpoint
user, dbname = os.environ["DB_USER"], os.environ["DB_NAME"]
rds = boto3.client("rds")
_conn, _exp = None, 0

def token():
    return rds.generate_db_auth_token(DBHostname=host, Port=3306,
                                      DBUsername=user, Region=region)  # ~15 min

def get_conn():
    global _conn, _exp
    now = time.time()
    if not _conn or now > _exp - 60:               # refresh early to avoid mid-query expiry
        _exp = now + 14 * 60
        _conn = pymysql.connect(host=host, user=user, password=token(),
                                db=dbname, ssl={"ssl": {}})
    return _conn

Hard limits to design for

  • Secrets Manager: GetSecretValue has a high RPS ceiling today, but still cache in memory (5-10 minutes) and back off on 429s.
  • IAM DB auth: tokens expire 15 minutes after issue - reconnect or re-token before then. A long-running query started on an about-to-expire token can fail mid-flight.
  • API Gateway / Data API: expect throttling. Use idempotency keys and full-jitter backoff, and push heavy writes to a queue (SQS, SNS, EventBridge) to smooth bursts.
  • Lambda async: AWS retries failed async invokes twice by default and queues for hours - combine with app-level retries carefully and use DLQs or on-failure destinations.
  • RDS Proxy: tune the idle timeout and MaxConnectionsPercent / MaxIdleConnectionsPercent. Don't assume the defaults match your DB's max_connections, and remember "healthy" isn't the same as "right-sized."

Best practices

  • Pick the path by workload shape. Many tiny stateless requests start with the Data API; chatty sessions, driver features, or strict latency favor IAM + RDS Proxy.
  • Cache secrets and tokens. Cache Secrets Manager reads in memory and rotate on TTL or version change; pre-emptively refresh IAM tokens at T-60s.
  • Make all writes idempotent. Use request IDs (an Idempotency-Key header or composite primary keys) so client retries don't double-insert.
  • Retry with full jitter. Exponential backoff with jitter, a cap on total time, and 429/5xx surfaced as 503 to upstreams.
  • Right-size RDS Proxy. Start around 80% MaxConnectionsPercent and 15% MaxIdleConnectionsPercent, then load-test and watch backend saturation during concurrency spikes.
  • Separate the hot paths. Drain high-variance writes into SQS or EventBridge and let consumers batch to the database - this cuts connection pressure and deadlocks.
  • Observability first. Emit attempt counters, retry reasons, token age, and pool stats; alarm on Secrets 429s, Data API 4xx/5xx, proxy connection errors, and Lambda DLQ depth.

Common pitfalls

  • Reading Secrets Manager on every invoke. Cache it - treat secrets as config, not data.
  • Letting IAM tokens lapse mid-query. Refresh early; on an auth error, re-mint and reconnect.
  • Assuming Lambda retries "handle it." They don't for sync calls, and async adds only two - you still need idempotency and backoff.
  • Oversizing the proxy idle pool. Idle connections still consume backend slots and can starve real traffic.
  • Ignoring API Gateway throttling. Model the quotas per stage and key, and test the 429 paths.

The short version

Connectionless doesn't mean "no database" - it means you scale calls, not connections. Choose the Data API for pure burst elasticity and easy ops; choose IAM + RDS Proxy for richer SQL with bounded sockets. Either way the production discipline is the same: cache secrets, refresh tokens early, engineer retries with jitter, enforce idempotency, and right-size the proxy or quotas. Do that and the fleet surges without shattering the database.

Leave a Comment