Cover function interaction with relational databases, key-value stores, document stores, and analytical stores. Explain connection management, pooling concerns, and throughput patterns.
Database access patterns in serverless systems are mostly about matching the data store to the access pattern and preventing bursty function concurrency from overwhelming the store. The function model does not remove database design. It makes weak database choices visible faster because every cold start, retry, and scale event puts pressure on connection limits, hot partitions, and inefficient query shapes.
The first decision is not “which database is modern.” It is “what kind of read and write behavior does this function really need?” Relational stores are strong for transactions and joins. Key-value stores are strong for direct lookup by key. Document stores help when aggregates evolve flexibly. Analytical stores are for scans, aggregates, and reporting workloads that should not live on the request path.
flowchart TD
A["Function invocation"] --> B{"Access pattern"}
B -->|transactional writes| C["Relational store"]
B -->|lookup by key| D["Key-value store"]
B -->|aggregate document| E["Document store"]
B -->|large scans or reporting| F["Analytical store"]
A --> G["Connection and concurrency limits"]
G --> C
G --> D
G --> E
G --> F
What to notice:
A serverless system often works best when different functions touch different persistence shapes:
The anti-pattern is forcing every access pattern through one database just because it already exists.
Traditional application servers often hold a stable pool of database connections. Serverless functions do not behave that way. A sudden concurrency spike can create many short-lived execution environments, each trying to open its own connections. That is how teams accidentally take down a relational database with a perfectly ordinary traffic burst.
Typical mitigations include:
1let dbClientPromise: Promise<DatabaseClient> | undefined;
2
3function getDbClient(): Promise<DatabaseClient> {
4 if (!dbClientPromise) {
5 dbClientPromise = createDatabaseClient({
6 endpoint: process.env.DB_ENDPOINT!,
7 connectionMode: "pooled",
8 });
9 }
10
11 return dbClientPromise;
12}
13
14export async function handler(orderId: string) {
15 const db = await getDbClient();
16 return db.queryOne("select * from orders where order_id = ?", [orderId]);
17}
What this demonstrates:
The right database can still struggle if each invocation performs too many small remote calls. Serverless systems benefit from:
This is especially important when the billing model punishes latency and the platform can scale faster than the database.
1function_profiles:
2 order-api:
3 store: relational
4 concurrency_limit: 50
5 idempotency-check:
6 store: key-value
7 access_pattern: single-key lookup
8 sales-dashboard-projector:
9 store: analytical
10 trigger: hourly-batch
An order API runs as functions behind an API gateway. Traffic spikes are short but intense. The team uses a relational database directly from each function and now sees connection exhaustion during promotions. What should be revisited first?
The stronger answer is access pattern and concurrency design, not just database size. The team likely needs connection pooling or proxying, lower request-path concurrency against that store, and possibly a split between transactional reads and high-volume derived reads. The issue is not that serverless and relational databases can never work together. The issue is treating bursty functions as if they were stable long-lived app servers.