Opinionated. Priced. Provisionable. The Stack.

What I build with right now, what I'd never touch, and why. The tools I recommend to clients right now, organised by what you're building, with prices, gotchas and fixes.

What are you building? →
Checked against the news every Monday. This week: Lambda becomes a web server, and AI learns to just decide →

Using an AI assistant? Tell it what you're building and ask it to read ask.cloudlesslabs.com, the map of every page here as Markdown. The whole stack is also in one file: the brief.

My biases, on the table

I'm an AWS consultant. I migrate on-premises systems to AWS, build secure, scalable serverless apps, integrate AI into existing systems, and coach teams to work with Claude Code and Codex.

None of this comes from reading reviews or comparing feature lists. It comes from two decades building software, the last 15 years of them in the cloud, running these tools in production for clients: the pain points are mine, the gotchas are the ones that bit me or my clients, and the fixes are the ones we shipped. Where I'm only watching something, or a verdict is still a proposal, the page says so.

This page is my opinion, and the biases are on the table. Every recommendation says what it costs, when to use it, when not to, what hurts, and how to fix the gotchas I've hit.

It isn't a static list. Every week I review what changed in the tools I use and the ones challenging them. When something new proves itself in my own tests, it earns a place here, and when a pick slips, its verdict changes. What changes week to week goes in the weekly report.

What are you building?

Pick a job. You get the stack I'd use, in order, what I'd avoid, and the worked example behind it. Every tool name opens its full entry.

  1. 01 An API for my app
  2. 02 An AI chat or LLM feature
  3. 03 Something live that many people watch at once
  4. 04 A traffic spike I can see coming
  5. 05 A website, SPA or PWA
  6. 06 Background jobs and retries
  7. 07 Event-driven plumbing
  8. 08 A job that runs for hours
  9. 09 Device and IoT data
  10. 10 Semantic search and RAG
  11. 11 Sending email
  12. 12 A database
  13. 13 A data lake
  14. 14 Provisioning everything
  15. 15 Private access and exposing a box
  16. 16 Logs without a shocking bill
  17. 17 An AWS foundation for a client
  18. 18 Moving on-premises systems to AWS
  19. 19 Building with AI agents
  20. 20 Sign-in and identity
  21. 21 Running the business

An API for my app

I need a backend API that reads and writes a database.

My stack for this

  1. 01
    Lambda function URLs

    Start here. Free front door, one function per concern.

  2. 02
    Neon (serverless Postgres)

    Postgres over HTTP, no VPC or proxy needed.

  3. 03
    Lambda Web Functions

    Once it's in your region, move HTTP logic to one Express app with a shared connection pool.

  4. 04
    Cloudflare Workers

    In front, if users are global or traffic is huge (auth, rate limits, caching).

Don't

  • GraphQL and AppSync

    GraphQL turns your schema into a second product to maintain, and almost no app benefits from it. Plain HTTP endpoints on function URLs do the same job with nothing extra to keep in sync.

    Instead: Lambda function URLs

  • API Gateway

    Useful, just not on day one. A function URL already gives you HTTPS, IAM auth, CORS and streaming with nothing else to configure. API Gateway adds routes, stages, deployments and integrations to keep in sync with your code, and a 29–30 second request timeout by default (a function URL allows 15 minutes). Add it when you need what only it does: per-customer API keys and usage plans, request validation, WAF on the endpoint, or writing straight into SQS. Function URLs use the same event format as HTTP APIs, so adding it later changes no code.

    Instead: Lambda function URLs

Worked example: Lambda, Web Functions or Workers? →

An AI chat or LLM feature

My app needs to call an LLM and stream the answer to users.

My stack for this

  1. 01
    Cloudflare Workers

    The proxy. Hides your key, enforces per-user budgets, streams tokens. Waiting is free.

  2. 02
    OpenRouter

    Every model behind one key. Pick by cost per intelligence.

  3. 03
    pgvector

    If the answers need your data (RAG).

  4. 04
    Cloudflare AI Gateway

    Caching, spend caps and fallback, on my shortlist.

Don't

  • AWS Lambda

    Not as the proxy. Lambda bills for every second the function waits on the model, and a streamed answer takes 10–60 seconds. A Worker bills CPU time only, so waiting is free: $33–167 vs about $0 extra for 1M chats. Keep Lambda for the work behind the proxy.

    Instead: Cloudflare Workers

  • Anthropic API (for client systems)

    Not for high-volume features in client products: cost per intelligence decides there, and OpenAI models through OpenRouter win it. (Claude Code stays my main development tool.)

    Instead: OpenRouter

Worked example: The LLM streaming proxy →

Something live that many people watch at once

Live auctions, collaborative editing, polls, chat rooms, game lobbies.

My stack for this

  1. 01
    Cloudflare Workers

    The door. Checks the login and routes each socket.

  2. 02
    Cloudflare Durable Objects

    The room. One referee per auction, document or room, holding every WebSocket.

  3. 03
    Neon (serverless Postgres)

    The record. Finished events are reconciled into Postgres.

  4. 04
    Amazon S3

    The archive. Bid logs and history as Parquet for analytics.

Don't

  • API Gateway

    WebSocket APIs bill every message pushed to every watcher, close connections after 2 hours, and hold no state, so you still need a database to decide who's winning. The auction example costs about $407 a month, vs $15–30 with one Durable Object per auction.

    Instead: Cloudflare Durable Objects

  • GraphQL and AppSync

    AppSync Events cost about $310 for the same auction, and bring GraphQL-era tooling. A Durable Object is the room and the referee in one.

    Instead: Cloudflare Durable Objects

Worked example: A live auction on Durable Objects →

A traffic spike I can see coming

A flash sale, ticket drop or launch where thousands hit at once.

My stack for this

  1. 01
    Cloudflare Workers

    The shock absorber. Workers start in milliseconds at any traffic, with no ramp-up and no concurrency quota, so 0 to 5,000 requests a second just works. Lambda scales fast but in steps: at most 1,000 new instances per 10 seconds per function, capped by the account default of 1,000, each one cold-starting. Junk is rejected at the edge.

  2. 02
    Cloudflare Durable Objects

    Exact stock counters. Instant "you got one" or "sold out", no overselling.

  3. 03
    AWS Lambda

    Calm checkout for the winners only. Behind the Worker and the counters, Lambda sees the 2,000 winners, not 50,000 people in ten seconds.

  4. 04
    Stripe

    Payment.

Don't

  • Lambda function URLs

    Not as the front door for the crowd. Function URLs are still my default for normal APIs, and Lambda still does the checkout above. The problem is pointing 50,000 people straight at it: the spike hits Lambda's scaling steps and 1,000-instance default, gets 429s, and every new instance opens a database connection at once. Put the Worker in front and only the winners reach Lambda.

    Instead: Cloudflare Workers

Worked example: The flash sale →

A website, SPA or PWA

A marketing site or a front-end app.

My stack for this

  1. 01
    Cloudflare static hosting

    Deploys from GitHub on every push. Static requests are free.

  2. 02
    Cloudflare R2

    Heavy media and downloads with zero egress fees.

  3. 03
    Astro on Cloudflare Workers

    When some pages must be rendered on request: content sites, docs, catalogs.

  4. 04
    Next.js on Cloudflare Workers (OpenNext)

    When it's a Next.js app that needs server rendering, without moving to Vercel.

  5. 05
    Cloudflare Turnstile

    Any form: Turnstile keeps the bots out, a Worker checks it and Resend sends the email.

Don't

  • Amazon CloudFront

    Not for a public site you can host anywhere. You'd assemble S3, origin access, a certificate and cache invalidations to get what Cloudflare gives you from a GitHub push, and pay-as-you-go egress can turn a viral page into a real bill where Cloudflare charges nothing. CloudFront stays right for all-AWS clients and private AWS origins.

    Instead: Cloudflare static hosting

Background jobs and retries

Work that runs after the request, with retries.

My stack for this

  1. 01
    Amazon SQS

    The queue. Set visibility timeout to six times the function timeout.

  2. 02
    AWS Lambda

    Batched consumers with partial batch failures.

  3. 03
    Amazon EventBridge

    Routing and schedules.

  4. 04
    Lambda durable functions

    Watching, for multi-step jobs with flaky dependencies.

Don't

  • AWS Step Functions

    A JSON state machine for logic that a few lines of JavaScript express better, and one more thing to deploy and debug. SQS with retries covers simple async work; Lambda durable functions are the candidate for multi-step jobs.

    Instead: Amazon SQS or Lambda durable functions

Event-driven plumbing

Moving events between services, buffering work, fanning out and streaming records into the lake.

My stack for this

  1. 01
    Amazon EventBridge

    Routing: send each event to whoever cares, by its content. Schedules too.

  2. 02
    Amazon SQS

    Work: buffer, retry, dead-letter, and let Lambda take messages in batches.

  3. 03
    Amazon SNS

    Fan-out: one event to several queues or functions.

  4. 04
    Amazon Data Firehose

    Streams into S3: records batched, compressed and written as files for the data lake, with no consumer to write.

  5. 05
    Amazon Kinesis

    Only when several consumers must replay the same ordered stream at high volume.

Don't

A job that runs for hours

A nightly import, report or migration longer than 15 minutes.

My stack for this

  1. 01
    Fargate scheduled tasks

    Runs only while the job runs, billed per second. Spot if it can restart.

  2. 02
    Amazon EventBridge

    Scheduler starts the task, with time zones.

Don't

Device and IoT data

Thousands of devices sending readings that end up in reports.

My stack for this

  1. 01
    AWS IoT Core

    Cheapest AWS front door if devices speak MQTT.

  2. 02
    Amazon SQS

    Buffer, then batch into Lambda.

  3. 03
    Neon (serverless Postgres)

    Postgres wins for ingest-and-report.

Don't

  • API Gateway

    A per-request HTTP front door for a stream of tiny messages. At 216M messages a month, the REST API alone costs about $756. The whole IoT Core pipeline, processing included, costs about $215–230.

    Instead: AWS IoT Core

  • Amazon SNS

    SNS invokes Lambda once per message: at device volumes that's millions of tiny invocations. SQS lets Lambda take them in batches.

    Instead: Amazon SQS

  • Cloudflare Durable Objects

    Each object only sees its own data, so there's no SQL across devices for reports. Use them only for sending commands back to devices; the readings belong in Postgres.

    Instead: Neon (serverless Postgres)

Worked example: IoT ingestion into Postgres →

Semantic search and RAG

Search by meaning, or feed an LLM your documents.

My stack for this

  1. 01
    pgvector

    When a user is waiting and results depend on your data and permissions.

  2. 02
    Amazon S3 Vectors

    When the collection is huge, cold or per client, and 100 ms is fine.

Don't

  • Amazon OpenSearch

    A cluster to size, patch and pay for around the clock, to do what Postgres full-text search plus pgvector already does next to your data. Only worth it for very large text corpora.

    Instead: pgvector

Worked example: pgvector or S3 Vectors? →

Sending email

Transactional emails and newsletters.

My stack for this

  1. 01
    Resend

    Both, on one platform, with an MCP server for agents.

  2. 02
    Amazon SES

    When data must stay in Australia.

Don't

  • Amazon Pinpoint

    AWS ends support on 30 October 2026. Don't start anything on it; move campaigns to Resend.

    Instead: Resend

  • Postmark

    The best deliverability reputation, but no marketing platform, so you'd run two email vendors. Resend does transactional and marketing in one, with idempotency keys and signed webhooks.

    Instead: Resend

A database

Where the data lives.

My stack for this

  1. 01
    Neon (serverless Postgres)

    Default. Branches, scale to zero, HTTP driver.

  2. 02
    Aurora PostgreSQL

    When the client must stay all-AWS.

Don't

A data lake

Analytics over everything the apps produce, cheaply, without a warehouse to run.

My stack for this

  1. 01
    Amazon S3

    The lake. Parquet (Iceberg next), partitioned by date and sorted by your second key.

  2. 02
    Amazon Data Firehose

    The way in. Streams records from apps and devices into S3, batched and compressed.

  3. 03
    S3 + Glue + Athena

    Big scans, ad-hoc questions and reports. Zero code, $5 per TB scanned.

  4. 04
    DuckDB

    App queries, writing Parquet, local dev and tests, on the same files. AWS bought the company behind it in August 2026.

  5. 05
    Aurora PostgreSQL

    If the app already runs on Aurora: join live tables with the lake in one SQL query (DuckDB inside).

Worked example: Athena or DuckDB? →

Provisioning everything

Infrastructure as code across AWS and every vendor.

My stack for this

  1. 01
    Pulumi

    Real code, S3 backend, and any Terraform provider.

Don't

  • AWS CloudFormation

    Never. YAML templates that only reach AWS. Pulumi does the same job in real code and provisions every other vendor too.

    Instead: Pulumi

Worked example: Pulumi everywhere →

Private access and exposing a box

Reach private AWS resources, or publish a service from a server.

My stack for this

  1. 01
    Tailscale

    Your machines into the VPC.

  2. 02
    Cloudflare Tunnel (cloudflared)

    A private service onto a public domain, no open ports.

Logs without a shocking bill

Observability that doesn't cost more than the app.

My stack for this

  1. 01
    Amazon CloudWatch

    The critical lines: WARN and ERROR, alarms, metric filters, Live Tail. JSON log format, retention set, no payloads.

  2. 02
    Amazon Data Firehose

    The chatty bulk, side by side: your logger batches DEBUG/INFO into Firehose, which writes to S3. About $75 per TB instead of about $673.

  3. 03
    DuckDB

    Search the S3 logs from a laptop or a Lambda when someone asks what happened.

  4. 04
    Cloudflare Workers

    Workers Logs moves to $0.25/GB on 1 December 2026.

Worked example: CloudWatch and Firehose, side by side →

An AWS foundation for a client

Accounts, access, backups and cost from day one.

My stack for this

  1. 01
    AWS Organizations + IAM Identity Center

    Accounts per environment, SSO for people.

  2. 02
    Pulumi

    Everything as code.

  3. 03
    AWS Backup

    Backup policies as code.

  4. 04
    Savings Plans and Reserved Instances

    Commit once the baseline is known.

  5. 05
    Cost Explorer (with Claude Code)

    Monthly review, with an agent doing the digging.

Moving on-premises systems to AWS

Lift databases and apps into AWS.

My stack for this

  1. 01
    Claude Code

    First step. It reverse-engineers the existing system: reads the source code, drives the CLIs, maps what's there, then writes the gap analysis and the migration plan, lift-and-shift or modernisation.

  2. 02
    AWS Database Migration Service

    Database migration with replication until cut-over.

  3. 03
  4. 04
    Pulumi

    The target infrastructure, as code.

Building with AI agents

How I write and test code now.

My stack for this

  1. 01
    Claude Code

    Writes the code.

  2. 02
    OpenAI Codex

    Clicks through the app and drives Playwright.

  3. 03
    Playwright, Vitest, pytest

    Agents need tests to know when they're done.

  4. 04
    Stryker mutation testing

    Proves the tests catch bugs.

  5. 05
    GitHub

    Where it all lives.

  6. 06
    Self-hosted MCP servers

    Scoped access to client systems.

Don't

Sign-in and identity

How users sign in, reset passwords and use Google, Microsoft or their company login.

My stack for this

  1. 01
    Better Auth

    First pick for a serious app on Postgres: users in your own database, one store, more features than Cognito, no per-user fee. You build the screens and wire the emails.

  2. 02
    Amazon Cognito

    The easy pick for small internal apps and admin panels: hosted login, reset emails and social sign-in out of the box, free under 10,000 users.

  3. 03
    Resend

    Sends Better Auth's reset, verification and magic-link emails.

  4. 04
    Cloudflare Workers

    Verifies the tokens at the edge, from either one.

Worked example: Better Auth or Cognito? →

Running the business

Payments, analytics and the daily tools.

My stack for this

  1. 01
    Stripe

    Payments.

  2. 02
    PostHog

    Analytics.

  3. 03
    GitHub

    Notes and docs as Markdown.

Don't

The map

Every tool on one screen, by layer. Solid means it's my default. Outlined: use it when it fits. Dashed: watching. Dotted: skip. Struck through: never.

Observability

Logs, metrics, alarms.

Decision guides

The worked examples behind the verdicts: real numbers, side by side, with the arithmetic.

  1. 01 Lambda, Web Functions or Workers? Where should this piece of logic run? Classic Lambda for events and heavy compute. Lambda Web Functions for HTTP business logic, once in your region. Workers at the edge, for waiting, spikes and real-time.
  2. 02 The LLM streaming proxy How do I add an AI chat feature without leaking my API key or paying for the model's thinking time? Never call an LLM from the browser. A Worker holds the key, checks the user's budget and streams tokens back. Waiting costs nothing on Workers.
  3. 03 The flash sale What happens when 50,000 people press "Buy" in the same ten seconds? Straight into Lambda, the spike meets scaling limits, 429s and an Aurora connection storm. With a Worker in front, Cloudflare absorbs it and AWS sees only the winners.
  4. 04 A live auction on Durable Objects How do thousands of bidders share one live auction without races, and what happens when it ends? One Durable Object per lot holds every WebSocket and processes bids one at a time. When the lot closes, it reconciles into AWS and deletes its data. $15–30 a month vs $130–410 on AWS designs.
  5. 05 IoT ingestion into Postgres 5,000 devices send a reading every minute. Where does the money go? For ingest-and-report, Postgres wins. The expensive part is the front door, not the database. Fix REST and per-message invocations, and the bill drops from about $1,100 to $215–410.
  6. 06 pgvector or S3 Vectors? Where should my embeddings live? Not competitors. pgvector when a user is waiting and results depend on your data. S3 Vectors when the collection is huge, cold or per client.
  7. 07 Pulumi everywhere Can I provision every tool on this page from one codebase? Almost. Pulumi uses any Terraform provider, so AWS, Cloudflare, GitHub, Tailscale, Neon, Resend, PostHog, OpenRouter and Stripe are all code.
  8. 08 CloudWatch and Firehose, side by side How do I keep chatty logs without a CloudWatch-sized bill? Not either-or. WARN and ERROR stay in CloudWatch, where you watch and alarm on them. The chatty bulk goes from your logger to Firehose to S3, at about $75 per TB instead of about $673.
  9. 09 Athena or DuckDB? How should I query my data lake, and what changed when AWS bought DuckDB's company? Use both. Athena for big scans and ad-hoc questions with zero code. DuckDB for app queries, writing Parquet and local development, on the same files. Sorting files, or Iceberg, fixes the second-index problem without copying data.
  10. 10 Better Auth or Cognito? Which sign-in should my app use, and what do I actually get for free? Not better or worse: one or the other, by job. Cognito for small internal apps and admin panels: everything out of the box, free under 10,000 users. Better Auth for serious apps on Postgres: users in your own database, more features, no per-user fee, and some building and patching.

Never.

What I'd steer any client away from, and why. Blunt on purpose.

  • Amazon DynamoDB

    I've never had a use case it fits better than Postgres with JSONB.

    Instead: Postgres on Neon, with JSONB for the document-shaped data.

  • Amazon Pinpoint

    Reaches end of support on 30 October 2026. Don't start anything on it.

    Instead: Resend, for transactional and marketing email on one platform.

  • AWS CloudFormation

    I hate it. Pulumi does the same job with real code.

    Instead: Pulumi: real code, an S3 backend, and every other vendor through Terraform providers.

  • AWS Step Functions

    Over-engineered. I've never seen the need in production.

    Instead: Plain code. SQS for simple async work; Lambda durable functions for multi-step jobs.

  • GitLab, Bitbucket and Atlassian

    Mediocre. Never recommended.

    Instead: GitHub, where agents, CI and every integration land first.

  • GraphQL and AppSync

    Over-engineering. Schema maintenance isn't worth it; plain web APIs are enough.

    Instead: Plain HTTP APIs on Lambda function URLs, and Durable Objects for real-time.

  • Jira, Asana, Monday.com and heavy project-management software

    A waste of time for a lean team.

    Instead: A minimal Trello board, and notes as Markdown in GitHub.

  • Lambda container images

    Worse cold starts, and they break Cognito triggers. I stopped using them.

    Instead: Zip-packaged Lambda functions, or Fargate when you really need a container.

  • Microsoft Azure, Windows and the Microsoft stack

    Years as a certified .NET developer taught me enough: a black box that puts dollars ahead of transparency and developer experience. Only worth a mention if it's 10–100x better.

    Instead: AWS, with Cloudflare at the edge.

  • Slack

    A distraction. Email and WhatsApp are enough.

    Instead: Email and WhatsApp for clients; notes and docs as Markdown in GitHub.

Gotchas, with fixes

91 things that bit me or my clients, each with the fix. This is the part nobody else writes down.

Amazon CloudFront Use when it fits

Gotcha: Pay-as-you-go egress can turn a viral page into a big bill.

Fix: Check the flat-rate plans, or put Cloudflare in front.

Gotcha: Cost Explorer labels log ingestion 'PutLogEvents', which looks like a per-call charge. It's billed per GB ingested.

Fix: Find the log groups with the most IncomingBytes. The cost is in bytes, not calls.

Gotcha: Logging whole message payloads in high-throughput functions. One function produced 59% of a client's 1 TB a month.

Fix: Log IDs and errors, not payloads. If payloads must be kept, send them to S3 or sample them.

Gotcha: The default Text log format can't drop DEBUG and INFO lines before ingestion.

Fix: Switch Lambda to JSON log format with application log level WARN in production.

Gotcha: Log groups never expire by default.

Fix: Set retention on every log group in your Pulumi code.

Gotcha: Infrequent Access log groups are half price but drop Live Tail, metric filters, subscription filters and Embedded Metric Format, which silently breaks Powertools Metrics. A log group's class can't be changed later.

Fix: Use IA only for verbose groups you rarely read, and create a new group (repoint LoggingConfig) to switch.

Gotcha: API Gateway execution logs at INFO level log every request.

Fix: Keep access logs, turn execution logs down.

Amazon Cognito My default

Gotcha: Cognito gives trigger Lambdas about 5 seconds. Cold container-image Lambdas blow through it and sign-in fails.

Fix: Keep triggers small and zip-packaged.

Gotcha: Cognito's built-in email sender is capped at 50 emails a day per AWS account.

Fix: Point the user pool at SES before going live.

Gotcha: Passkeys need the username first, and can only be registered after a first sign-in.

Fix: Offer passkeys as an upgrade after sign-in, not as the first-time path.

Gotcha: Direct PUT bills every record rounded up to 5 KB. One 200-byte log line per record costs 25 times the list price, about $925 per TB.

Fix: Pack many newline-delimited lines into each record (up to about 1,000 KB). Records of 100 KB or more make the rounding negligible.

Gotcha: Lambda freezes the environment when the handler returns, so buffered lines can be lost.

Fix: Await a flush before returning (try/finally), flush on size, and send WARN/ERROR synchronously to console so CloudWatch keeps the critical lines.

Gotcha: A stream accepts 1 MiB/s by default in Sydney. Busy peaks throttle.

Fix: Request a higher limit up front, and retry the records reported in FailedPutCount.

Gotcha: Lambda's built-in log destination to Firehose is not the cheap path: CloudWatch charges $0.335/GB to deliver it, then Firehose charges $0.17/GB for vended logs.

Fix: Call PutRecordBatch from your own logger instead.

Gotcha: Dynamic partitioning looks handy and adds about $25 per TB plus per-object and per-hour fees.

Fix: Use the default YYYY/MM/DD/HH prefix (free) and one stream per app or per table.

Gotcha: Reads are eventually consistent by default, so a read right after a write can miss it.

Fix: Use strongly consistent reads (tables only, not global secondary indexes), or use Postgres.

Gotcha: Existing Pinpoint campaigns and journeys stop being supported on 30 October 2026.

Fix: Move marketing email to Resend (or SES with your own list tool).

Amazon S3 Vectors Use when it fits

Gotcha: No hybrid (keyword + vector) search.

Fix: Search pgvector and S3 Vectors separately and merge with Reciprocal Rank Fusion, or put OpenSearch's s3vector engine on top at scale.

Amazon SES Use when it fits

Gotcha: New SES accounts silently default to the Essentials plan at $0.16 per 1,000, 60% more than à la carte.

Fix: Choose the à la carte plan explicitly if you don't need Essentials features.

Gotcha: Sandbox exit is per region, and AWS quotes about 96 hours for a first response.

Fix: Request production access on day one of the project, in every region you'll send from.

Amazon SNS Use when it fits

Gotcha: SNS → Lambda invokes a function per message: at IoT volumes that's millions of tiny invocations.

Fix: SNS → SQS → Lambda with batching, or skip SNS and write straight to SQS.

Amazon SQS My default

Gotcha: Messages reappear and get processed twice, or go to the DLQ, when the queue's visibility timeout is too close to the function's timeout.

Fix: Set the visibility timeout to at least six times the function timeout.

Gotcha: One bad message in a batch fails the whole batch, and the good messages retry too.

Fix: Enable ReportBatchItemFailures and return only the failed message IDs.

Gotcha: Messages bounce to the DLQ after too few attempts, or loop forever.

Fix: Set maxReceiveCount deliberately (for example 3–5), and alarm on DLQ depth.

API Gateway Use when it fits

Gotcha: REST API pricing ($3.50/M) is almost three times HTTP API pricing ($1.29/M).

Fix: Default to HTTP APIs, or a function URL, unless you need a REST-only feature.

Gotcha: Adding API Gateway later looks like a rewrite.

Fix: It isn't. Function URLs use the same event format as HTTP APIs (payload version 2.0), so the same handler works behind either.

Gotcha: API Gateway execution logs at INFO level log every request into CloudWatch.

Fix: Keep access logs; turn execution logging down or off in production.

Gotcha: Astro 6 changed how the Cloudflare adapter exposes bindings: Astro.locals.runtime is deprecated.

Fix: Read bindings (KV, R2, secrets) through env instead, and check the adapter's upgrade notes when you move between major versions.

Aurora PostgreSQL Use when it fits

Gotcha: RDS Proxy 'pins' a session to one connection when it sees SET, prepared statements, temporary tables, advisory locks or statements over 16 KB. Pinned sessions exhaust the pool.

Fix: Watch the DatabaseConnectionsCurrentlySessionPinned metric and remove the pinning causes. Or move HTTP logic to Lambda Web Functions, which share one pool across up to 128 requests.

Gotcha: Serverless v2 can pause at 0 ACU, but resuming takes about 15 seconds (30+ after a day paused).

Fix: Don't pause production databases that serve users.

AWS Lambda My default

Gotcha: A function idle for weeks goes Inactive, and the first calls fail while it reactivates.

Fix: Ping it on a schedule, use provisioned concurrency for critical paths, or move bursty HTTP endpoints to Workers.

Gotcha: Scaling is capped: 1,000 concurrent executions per account by default, and each function adds at most 1,000 instances every 10 seconds.

Fix: The limit is requests in flight, not per second (2,000 req/s at 100 ms is only 200). Raise the quota before launches, and put Workers in front of known spikes.

Gotcha: SnapStart doesn't help Node.js. As of October 2026 it only supports Java, Python, .NET and container images.

Fix: For Node.js zip functions, keep the bundle small, initialise lazily, and use provisioned concurrency where latency matters.

Gotcha: Log-level filtering (drop DEBUG and INFO before you pay for them) only works with the JSON log format. The default Text format logs everything.

Fix: Set the log format to JSON and the application log level to WARN in production.

Gotcha: Response streaming isn't available through a function URL when the function is in a VPC.

Fix: Keep streaming endpoints out of the VPC, or stream from Workers or Lambda Web Functions.

Gotcha: Long-lived IAM user keys scattered across laptops.

Fix: SSO through Identity Center for every human; no static keys.

Better Auth My default

Gotcha: There's no hosted login page. Better Auth gives you the endpoints and a client SDK, not the screens.

Fix: Build sign-in, sign-up and reset-password pages in your app (a day with Claude Code), or use a ready-made component kit if the front end is React. They match your design, which Cognito's pages never quite do.

Gotcha: It doesn't send email. Password reset, verification and magic links produce a link and wait for you.

Fix: Pass it to Resend in a few lines (sendResetPassword, sendVerificationEmail, sendMagicLink).

Gotcha: 27 security advisories in 2026 (3 critical), many in the young SSO and SCIM plugins. Other open-source auth projects had similar years.

Fix: Pin exact versions, let Renovate or Dependabot open update PRs, and run a weekly scheduled agent that checks the repo's security advisories and opens the upgrade. Enable only the plugins you use.

Gotcha: Letting users register their own SSO providers caused most of the 2026 SSO advisories (account takeover through unproven domains).

Fix: Register SSO providers only from your own server code, require DNS domain verification, turn on requireTimestamps for SAML, and keep implicit account linking off.

Gotcha: Its JWT plugin signs EdDSA by default, which API Gateway JWT authorizers and IAM don't accept.

Fix: Configure RS256. Then Workers, API Gateway and Cognito identity pools all accept its tokens.

Gotcha: Google Workspace can't push users to custom apps over SCIM.

Fix: Google sign-in works; for Google directory sync, use another tool (Ory Polis or WorkOS) or invite users by domain.

Gotcha: Single-threaded isn't automatically transactional across an await. If you await an outside call (asking AWS whether a bidder is verified) between reading the price and writing the bid, other bids run in the gap.

Fix: Do outside checks before the critical section. Keep read → check → write → broadcast synchronous (the SQLite API is synchronous, or use transactionSync).

Gotcha: Every deploy restarts all Durable Objects and drops every WebSocket.

Fix: Clients must reconnect automatically. Don't deploy during a live event.

Gotcha: No published maximum of WebSockets per object. The real ceilings are one thread, 128 MB and about 1,000 incoming requests a second.

Fix: Up to about 2,000 watchers connect directly. Above that, fan out through helper objects that each hold 1,000 to 2,000 sockets, and batch broadcasts every 50–100 ms.

Gotcha: Alarms can fire more than once.

Fix: Make the alarm handler idempotent: check the object's state before acting.

Gotcha: An object is created near its first request (or where locationHint says) and never moves.

Fix: Pass locationHint 'oc' on first access for Australian users.

Gotcha: 10 GB per object, 100 columns per table, 2 MB per row.

Fix: Design the schema around one entity (this auction, this room), and export finished data to AWS.

Cloudflare Queues Skip Proposed

Gotcha: Without a dead-letter queue, failed messages are deleted.

Fix: Always configure a DLQ.

Cloudflare Sandbox Use when it fits

Gotcha: Untrusted code can still try to reach your APIs or leak secrets through the network.

Fix: Never put secrets inside the sandbox. Route its traffic through an outbound Worker, the egress proxy that injects credentials per destination so the code never sees them, and allow only the destinations it needs.

Gotcha: Costs grow with every session left running.

Fix: Give every session a timeout and destroy it when the user leaves; back up the workspace and restore it if they come back.

Gotcha: The widget alone doesn't stop bots: they post straight to your endpoint and never load it.

Fix: Check every token on the server, with your secret, at https://challenges.cloudflare.com/turnstile/v0/siteverify, and reject the request unless it succeeds.

Gotcha: A token can be checked only once and expires after 5 minutes, so a slow form or a retry fails.

Fix: Verify once, at submission; if the form errors, reset the widget for a new token before the user resubmits.

Gotcha: Some bots still get through: Turnstile tells you a browser was there, not that a human meant it.

Fix: Add a hidden honeypot field and drop any submission that fills it.

Gotcha: It's not full Node.js. Most pure-JavaScript npm packages work, but there are no native modules and child_process, worker_threads, cluster and vm are stubs.

Fix: Treat Workers as the layer in front of your backend. If a Worker grows into business logic that needs these, move it to Lambda Web Functions.

Gotcha: Memory is fixed at 128 MB per isolate.

Fix: Stream instead of buffering. Anything heavy goes to Lambda.

Gotcha: Calling AWS from a Worker means storing access keys: there's no IAM role.

Fix: Use narrowly scoped keys in Workers secrets, or call an AWS endpoint (function URL with signed requests) instead of AWS APIs directly.

Gotcha: Cron triggers are UTC only.

Fix: Convert schedules to UTC, or schedule in AWS with EventBridge Scheduler.

Gotcha: Workers Logs bills per log event until 30 November 2026 (20M included, then $0.60 per million). Five console.log lines per request multiplies the bill by five.

Fix: Log one structured object per request. From 1 December 2026 billing moves to $0.25/GB, cheaper than CloudWatch in Sydney ($0.67/GB).

Gotcha: Operation names can mislead: CloudWatch log ingestion shows as 'PutLogEvents' but is billed per GB.

Fix: Group by usage type as well as operation.

DuckDB Use when it fits

Gotcha: Lambda has no HOME directory, so INSTALL, LOAD and CREATE SECRET fail with "Can't find the home directory".

Fix: Run SET home_directory='/tmp' first.

Gotcha: The S3, AWS and Iceberg extensions are downloaded at runtime unless bundled: about 100 MB on every cold start.

Fix: Bundle them in the layer and SET extension_directory to it.

Gotcha: DuckDB SQL isn't Athena's (Trino) SQL. Date, array and approximate functions differ.

Fix: Test shared queries on both engines, or keep each query on one engine.

Gotcha: The layer zip is over 50 MB.

Fix: Upload the layer through S3, or use a container image.

Fargate scheduled tasks Use when it fits

Gotcha: Fargate Spot tasks can be stopped with two minutes' notice.

Fix: Use Spot only for jobs that checkpoint or can safely restart.

Gotcha: Cognito gives trigger functions about 5 seconds. A cold container image can blow through that.

Fix: Keep Cognito triggers small, zip-packaged and Node.js.

Lambda durable functions Watching Proposed

Gotcha: Whether retries are billed as extra operations isn't verified.

Fix: Pilot on one workload and read the bill before rolling out.

Gotcha: Response streaming doesn't work through a function URL for a function inside a VPC.

Fix: Keep streaming functions outside the VPC, or use Workers or Lambda Web Functions.

Gotcha: Up to 128 requests share one process. Lambda code that keeps the current user or tenant in a module-level variable becomes a data leak between users.

Fix: Audit global variables before migrating. Keep per-request data in AsyncLocalStorage or pass it explicitly.

Gotcha: A brand-new Web Function starts with a conservative per-environment concurrency limit and learns the right one from traffic. Your first big burst uses more environments and cold-starts more.

Fix: Load-test before launch and judge by the second run. Lambda remembers what it learned, even after scaling to zero.

Gotcha: No minimum or reserved environments, and AWS doesn't publish how fast new ones are added. A burst faster than the spare headroom waits on cold starts, and over the limits requests get HTTP 429, not a queue.

Fix: Clients retry with backoff and jitter. For known spikes, put Workers in front.

Gotcha: Default limits per region: 10,000 req/s per endpoint, 100,000 per account, 2,000 vCPUs (1,000 environments).

Fix: Request increases ahead of growth, and set rateLimit on endpoints to protect Postgres.

Gotcha: Node.js 24 on Arm64 only, 2 GB and 2 vCPU per environment.

Fix: Keep anything bigger on classic Lambda.

Gotcha: The pooled endpoint accepts 10,000 clients, but real concurrent transactions are about 90% of max_connections: roughly 94 at 0.25 CU, 377 at 1 CU. Queued clients time out after 120 s.

Fix: Size compute for concurrent transactions, not client count. Use the HTTP driver for one-shot queries.

Gotcha: The HTTP driver has no interactive transactions.

Fix: Use the WebSocket mode of the serverless driver when you need a session.

Gotcha: After 5 minutes idle, compute scales to zero and the next query waits a few hundred milliseconds.

Fix: Fine for most apps. Disable scale-to-zero on production branches that need instant answers.

Gotcha: The build goes over the Worker size limit.

Fix: Run npx @opennextjs/cloudflare build and read what's in the bundle; heavy server-only dependencies are the usual cause.

Gotcha: ISR pages serve stale content longer than expected when the cache sits in Workers KV, which is eventually consistent.

Fix: Use R2 for the incremental cache, as OpenNext's docs recommend (and D1 for the tag cache).

OpenRouter My default

Gotcha: Calling it from the browser exposes your API key.

Fix: Always go through a server-side proxy. A Cloudflare Worker is ideal: see the LLM proxy guide.

pgvector My default

Gotcha: The HNSW index has to fit in memory, so large collections force a big, always-on database.

Fix: Keep the hot data in pgvector and move the archive to S3 Vectors. Watch Neon's Lakebase Search, which keeps the index on disk.

Gotcha: The unscoped npm package postmark-mcp was malware that BCC'd every email to an attacker.

Fix: If you use Postmark with agents, the official package is @activecampaign/postmark-mcp.

Pulumi My default

Gotcha: A vendor has no Pulumi package.

Fix: Run pulumi package add terraform-provider <namespace>/<name>: Pulumi generates an SDK from any Terraform or OpenTofu provider.

Gotcha: Community providers are often maintained by one person (Neon, Resend, Clerk).

Fix: Pin provider versions and upgrade deliberately.

Gotcha: New AWS services can land in @pulumi/aws-native before @pulumi/aws (Lambda Web Functions today).

Fix: Use aws-native for those resources alongside the classic provider.

Gotcha: Pulumi doesn't bundle Cloudflare Worker code.

Fix: Build with Wrangler, deploy the output with Pulumi, and give each Worker one owner.

Resend My default

Gotcha: Young company (founded 2023).

Fix: Keep sending behind one small module of your own so a switch would be a day's work, not a rewrite.

Gotcha: The Terraform provider is community-maintained.

Fix: Pin its version and keep templates in code, not in IaC.

S3 + Glue + Athena Use when it fits

Gotcha: Data partitioned by date, queried by customer: every query scans every partition.

Fix: Keep the date partitions and sort each file by the second key (DuckDB COPY ... ORDER BY). Athena and DuckDB skip row groups by their min/max statistics, and Athena bills only the bytes it reads. For a real fix, use Iceberg tables in S3 Tables, partitioned by day(ts) plus bucket(N, customer_id), with sort or z-order compaction.

Gotcha: Partition projection and Glue partition indexes look like secondary indexes. They only speed up query planning.

Fix: Use sorting, bucketing or Iceberg to prune data; projection to skip catalog lookups.

Gotcha: CTAS and INSERT INTO write at most 100 partitions per query.

Fix: Write Parquet with DuckDB COPY (no limit, full control of sort order and file size), or split the job.

Self-hosted MCP servers Use when it fits

Gotcha: Unofficial MCP packages can be malicious (the fake postmark-mcp BCC'd every email to an attacker).

Fix: Use vendors' official servers, pin versions, and read the code of anything community-made.

Want this stack in your AWS account?

Cloudless Labs is a Sydney consultancy. We migrate on-premises systems to AWS, build serverless apps on this stack, and coach teams to ship with Claude Code and Codex.

Contact Us

For general inquiries, please contact us at:

For e-learning capabilities, please contact: