Serverless is a cloud model where you run code without managing servers. Despite the name, servers still exist — you just don't provision, scale, or maintain them. You write a function, deploy it, and pay only for the time it actually runs. If Cartara flagged this in your diff, you likely added a serverless function, changed how one handles state, or hit a pattern that breaks in a serverless environment (like holding a database connection open).
How it works
When a request comes in, the cloud provider spins up a container to run your code, executes the function, and tears the container down (or keeps it warm briefly for future calls). You don't decide how many copies run — the platform handles that automatically.
Cold start vs warm start is the key trade-off:
- Cold start: A fresh container has to spin up. This adds latency — typically 100ms to 2 seconds depending on the runtime and provider
- Warm start: A recently-used container is reused. Near-instant execution
Cold starts matter most for latency-sensitive user-facing code. They matter less for background tasks.
What triggers a serverless function
Functions don't run on their own. They respond to events:
| Trigger | Example |
|---|---|
| HTTP request | API call via API Gateway |
| Schedule | Cron job (every morning at 6am) |
| Queue message | New item in a queue |
| File upload | Image uploaded to S3 |
| Database change | New row inserted |
| Auth event | User signs up |
The statelessness constraint
Serverless functions are stateless by design. Each invocation is independent — you can't rely on in-memory data persisting between calls. One run might happen on a completely different container than the last one.
This means any state that needs to persist across requests must live outside the function: in a database, object storage, or a cache. This is the most common thing builders get wrong.
```javascript
// Broken: assumes data persists in memory between calls
let callCount = 0 // this resets on every cold start
export async function handler(event) {
callCount++ // unreliable across instances
return { count: callCount }
}
// Correct: state stored in a database
export async function handler(event) {
const result = await db.counters.upsert({
where: { id: 'global' },
update: { count: { increment: 1 } },
create: { id: 'global', count: 1 }
})
return { count: result.count }
}
```
When serverless is a good fit
- Unpredictable or spiky traffic — scales to zero and to thousands instantly
- Event-driven workloads — responding to uploads, messages, webhooks
- Background tasks — image processing, email sending, data transformation
- APIs with variable load — you pay only for actual usage
- Side projects — generous free tiers make it nearly free at low volume
When serverless is a poor fit
- Long-running processes — most providers cap execution time (AWS Lambda: 15 minutes max)
- Latency-critical paths where cold starts are unacceptable
- Stateful workloads that need persistent in-memory state
- High sustained throughput — at scale, always-on containers often cost less
- ML model serving with large models that take too long to load
The database connection problem
This trips up many serverless builders. A traditional server opens a database connection once and reuses it. A serverless function might spin up 1,000 instances simultaneously — each wanting its own connection. Most databases have a connection limit, and you'll exhaust it.
The fix is a connection pooler (PgBouncer, or the built-in pooler on Supabase/Neon). It maintains a shared pool of connections that functions borrow and return. Enable this before deploying serverless functions that touch a database.
```javascript
// Use the pooled connection string, not the direct one
// Supabase example:
const db = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL // should point to PgBouncer/pooler, not direct DB
}
}
})
```
What You'll See in Your Code
Cartara commonly flags synchronous fan-out patterns that create cascading latency in serverless:
```javascript
// Slow: calling multiple lambdas synchronously
export async function processOrder(event) {
await sendConfirmationEmail(event.orderId) // wait 300ms
await updateInventory(event.orderId) // wait 200ms
await notifyWarehouse(event.orderId) // wait 400ms
// Total: ~900ms, and each failure blocks the others
}
// Better: trigger async jobs via a queue
export async function processOrder(event) {
await queue.sendBatch([
{ type: 'send_confirmation', orderId: event.orderId },
{ type: 'update_inventory', orderId: event.orderId },
{ type: 'notify_warehouse', orderId: event.orderId },
])
return { status: 'queued' } // returns fast, work happens in background
}
```
Providers at a glance
| Provider | Service | Max Timeout | Notes |
|---|---|---|---|
| AWS | Lambda | 15 minutes | Most mature, largest ecosystem |
| Google Cloud | Cloud Run | 60 minutes | Container-based, very flexible |
| Cloudflare | Workers | 30s CPU time | Edge-native, very fast globally |
| Vercel | Functions | 60 seconds | Best DX for Next.js apps |
Serverless vs containers
Serverless wins at low traffic, simplicity, and zero idle cost. Containers win at sustained high throughput, long-running processes, and predictable cost. The line is blurring — services like Cloud Run and AWS Fargate are "serverless containers" where you define a container but the platform manages scaling.