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.
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.
AWS-first
AWS has a service for almost every enterprise need. It's my default cloud.
Not Well-Architected to the letter
Good ideas, but not one-size-fits-all, and biased towards maximising AWS spend. The client's context wins.
Infrastructure as code, or it doesn't exist
A service's greatness includes whether I can provision it with Pulumi or Terraform. If I can't, it loses points.
Microsoft only at 10–100x
I was a certified .NET developer for a long time, and Microsoft has let me down more times than I can count. It's a black box that puts dollars ahead of transparency and developer experience. Microsoft gets a mention only when it is 10 to 100 times better than the alternative. It rarely clears that bar.
Google Cloud is not enterprise-complete
Fine products, not enough coverage for the enterprises I work with.
Cloudflare is the edge
Very cheap and very good at the edge. I use it next to AWS, not instead of it.
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.
- 01 An API for my app
- 02 An AI chat or LLM feature
- 03 Something live that many people watch at once
- 04 A traffic spike I can see coming
- 05 A website, SPA or PWA
- 06 Background jobs and retries
- 07 Event-driven plumbing
- 08 A job that runs for hours
- 09 Device and IoT data
- 10 Semantic search and RAG
- 11 Sending email
- 12 A database
- 13 A data lake
- 14 Provisioning everything
- 15 Private access and exposing a box
- 16 Logs without a shocking bill
- 17 An AWS foundation for a client
- 18 Moving on-premises systems to AWS
- 19 Building with AI agents
- 20 Sign-in and identity
- 21 Running the business
An API for my app
I need a backend API that reads and writes a database.
My stack for this
- 01Lambda function URLs
Start here. Free front door, one function per concern.
- 02Neon (serverless Postgres)
Postgres over HTTP, no VPC or proxy needed.
- 03Lambda Web Functions
Once it's in your region, move HTTP logic to one Express app with a shared connection pool.
- 04Cloudflare 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
An AI chat or LLM feature
My app needs to call an LLM and stream the answer to users.
My stack for this
- 01Cloudflare Workers
The proxy. Hides your key, enforces per-user budgets, streams tokens. Waiting is free.
- 02OpenRouter
Every model behind one key. Pick by cost per intelligence.
- 03pgvector
If the answers need your data (RAG).
- 04Cloudflare 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
Something live that many people watch at once
Live auctions, collaborative editing, polls, chat rooms, game lobbies.
My stack for this
- 01Cloudflare Workers
The door. Checks the login and routes each socket.
- 02Cloudflare Durable Objects
The room. One referee per auction, document or room, holding every WebSocket.
- 03Neon (serverless Postgres)
The record. Finished events are reconciled into Postgres.
- 04Amazon 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
A traffic spike I can see coming
A flash sale, ticket drop or launch where thousands hit at once.
My stack for this
- 01Cloudflare 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.
- 02Cloudflare Durable Objects
Exact stock counters. Instant "you got one" or "sold out", no overselling.
- 03AWS 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.
- 04Stripe
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
A website, SPA or PWA
A marketing site or a front-end app.
My stack for this
- 01Cloudflare static hosting
Deploys from GitHub on every push. Static requests are free.
- 02Cloudflare R2
Heavy media and downloads with zero egress fees.
- 03Astro on Cloudflare Workers
When some pages must be rendered on request: content sites, docs, catalogs.
- 04Next.js on Cloudflare Workers (OpenNext)
When it's a Next.js app that needs server rendering, without moving to Vercel.
- 05Cloudflare 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
- 01Amazon SQS
The queue. Set visibility timeout to six times the function timeout.
- 02AWS Lambda
Batched consumers with partial batch failures.
- 03Amazon EventBridge
Routing and schedules.
- 04Lambda 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
- 01Amazon EventBridge
Routing: send each event to whoever cares, by its content. Schedules too.
- 02Amazon SQS
Work: buffer, retry, dead-letter, and let Lambda take messages in batches.
- 03Amazon SNS
Fan-out: one event to several queues or functions.
- 04Amazon Data Firehose
Streams into S3: records batched, compressed and written as files for the data lake, with no consumer to write.
- 05Amazon Kinesis
Only when several consumers must replay the same ordered stream at high volume.
Don't
- AWS Step Functions
A JSON state machine where a queue and a few lines of JavaScript do the job.
Instead: Amazon SQS or Lambda durable functions
A job that runs for hours
A nightly import, report or migration longer than 15 minutes.
My stack for this
- 01Fargate scheduled tasks
Runs only while the job runs, billed per second. Spot if it can restart.
- 02Amazon EventBridge
Scheduler starts the task, with time zones.
Don't
- Lambda container images
Not a workaround for long jobs: a container image still stops at Lambda's 15 minutes, with worse cold starts. If it needs a container and hours, run it on Fargate.
Instead: Fargate scheduled tasks
Device and IoT data
Thousands of devices sending readings that end up in reports.
My stack for this
- 01AWS IoT Core
Cheapest AWS front door if devices speak MQTT.
- 02Amazon SQS
Buffer, then batch into Lambda.
- 03Neon (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)
Semantic search and RAG
Search by meaning, or feed an LLM your documents.
My stack for this
- 01pgvector
When a user is waiting and results depend on your data and permissions.
- 02Amazon 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
Sending email
Transactional emails and newsletters.
My stack for this
- 01Resend
Both, on one platform, with an MCP server for agents.
- 02Amazon 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
- 01Neon (serverless Postgres)
Default. Branches, scale to zero, HTTP driver.
- 02Aurora PostgreSQL
When the client must stay all-AWS.
Don't
- Amazon DynamoDB
You design every query up front and pay for it in rewrites when the app changes. Postgres with JSONB gives you documents and SQL in one place, and covers every case I've met.
Instead: Neon (serverless Postgres)
- Cloudflare D1
A thin business case and no tooling to migrate thousands of databases. For per-tenant SQLite, use the primitive D1 is built on; for a normal database, use Postgres.
Instead: Cloudflare Durable Objects or Neon (serverless Postgres)
A data lake
Analytics over everything the apps produce, cheaply, without a warehouse to run.
My stack for this
- 01Amazon S3
The lake. Parquet (Iceberg next), partitioned by date and sorted by your second key.
- 02Amazon Data Firehose
The way in. Streams records from apps and devices into S3, batched and compressed.
- 03S3 + Glue + Athena
Big scans, ad-hoc questions and reports. Zero code, $5 per TB scanned.
- 04DuckDB
App queries, writing Parquet, local dev and tests, on the same files. AWS bought the company behind it in August 2026.
- 05Aurora PostgreSQL
If the app already runs on Aurora: join live tables with the lake in one SQL query (DuckDB inside).
Provisioning everything
Infrastructure as code across AWS and every vendor.
My stack for this
- 01Pulumi
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
Private access and exposing a box
Reach private AWS resources, or publish a service from a server.
My stack for this
- 01Tailscale
Your machines into the VPC.
- 02Cloudflare 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
- 01Amazon CloudWatch
The critical lines: WARN and ERROR, alarms, metric filters, Live Tail. JSON log format, retention set, no payloads.
- 02Amazon 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.
- 03DuckDB
Search the S3 logs from a laptop or a Lambda when someone asks what happened.
- 04Cloudflare Workers
Workers Logs moves to $0.25/GB on 1 December 2026.
An AWS foundation for a client
Accounts, access, backups and cost from day one.
My stack for this
- 01AWS Organizations + IAM Identity Center
Accounts per environment, SSO for people.
- 02Pulumi
Everything as code.
- 03AWS Backup
Backup policies as code.
- 04Savings Plans and Reserved Instances
Commit once the baseline is known.
- 05Cost 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
- 01Claude 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.
- 02AWS Database Migration Service
Database migration with replication until cut-over.
- 03AWS Organizations + IAM Identity Center
The landing spot.
- 04Pulumi
The target infrastructure, as code.
Building with AI agents
How I write and test code now.
My stack for this
- 01Claude Code
Writes the code.
- 02OpenAI Codex
Clicks through the app and drives Playwright.
- 03Playwright, Vitest, pytest
Agents need tests to know when they're done.
- 04Stryker mutation testing
Proves the tests catch bugs.
- 05GitHub
Where it all lives.
- 06Self-hosted MCP servers
Scoped access to client systems.
Don't
- GitLab, Bitbucket and Atlassian
Mediocre tools that teams tolerate out of habit. Agents, CI and every integration land on GitHub first.
Instead: GitHub
Sign-in and identity
How users sign in, reset passwords and use Google, Microsoft or their company login.
My stack for this
- 01Better 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.
- 02Amazon 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.
- 03Resend
Sends Better Auth's reset, verification and magic-link emails.
- 04Cloudflare Workers
Verifies the tokens at the edge, from either one.
Running the business
Payments, analytics and the daily tools.
My stack for this
Don't
- Slack
A distraction: always-on chat fragments the day. Email and WhatsApp are enough for clients, and notes and docs live as Markdown in GitHub.
Instead: GitHub
- Jira, Asana, Monday.com and heavy project-management software
Process for the sake of process. Jira, Asana and Monday.com turn a small team into ticket administrators. A minimal board is enough.
Instead: Trello
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.
Edge
Between your users and your backend.
Compute
Where business logic runs.
APIs, queues and events
How requests and events move.
- Default Amazon Data Firehose: Managed delivery of records into S3 (and other sinks)
- Default Amazon EventBridge: Events and schedules
- Default Amazon SQS: Queues and back-pressure
- Default Lambda function URLs: HTTP front door for Lambda
- Use when Amazon Kinesis: High-throughput streaming
- Use when Amazon SNS: Pub/sub fan-out
- Use when API Gateway: Managed API front door
- Use when · Proposed AWS IoT Core: Device ingestion
- Watching · Proposed Lambda durable functions: Durable multi-step jobs
- Skip · Proposed Cloudflare Queues
- Never AWS Step Functions
- Never GraphQL and AppSync
Data
Databases, migrations and the data lake.
- Default AWS Database Migration Service: Database migrations
- Default Neon (serverless Postgres): Default database
- Use when Aurora PostgreSQL: AWS-native Postgres
- Use when DuckDB: Embedded analytics engine
- Use when S3 + Glue + Athena: Data lake
- Skip Amazon OpenSearch
- Skip Cloudflare D1
- Skip Cloudflare Hyperdrive
- Never Amazon DynamoDB
AI
Models, embeddings and agent access.
- Default OpenRouter: LLM access for client systems
- Default pgvector: Vectors in your app's database
- Use when Amazon S3 Vectors: Vector archive
- Use when Cloudflare Sandbox: Running untrusted and AI-written code
- Use when Self-hosted MCP servers: Agent integrations
- Watching · Proposed Cloudflare AI Gateway: LLM traffic control
- Skip Anthropic API (for client systems)
- Skip · Proposed Cloudflare Workers AI
Transactional and marketing.
Storage and delivery
Files, websites and CDNs.
Observability
Logs, metrics, alarms.
Infrastructure as code
If I can't provision it, it loses points.
Networking and hosting
Private access, exposure, DNS, servers.
Accounts, identity and cost
The AWS foundation.
- Default Amazon Cognito: Authentication
- Default AWS Backup: Backups
- Default AWS Organizations + IAM Identity Center: Accounts and access
- Default Better Auth: Authentication library, users in your Postgres
- Default Cost Explorer (with Claude Code): Cost analysis
- Default Savings Plans and Reserved Instances: Cost
Build and test
How the code gets written and checked.
Business tools
Payments, analytics, the daily kit.
Platforms
The clouds I don't pick.
Nothing matches. Clear the filters.
Decision guides
The worked examples behind the verdicts: real numbers, side by side, with the arithmetic.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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 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.
Skip: fine tools I don't need
- Amazon OpenSearch
Overkill for most apps. Postgres full-text plus pgvector covers search.
Instead: Postgres full-text search plus pgvector, next to the data you already have.
- Anthropic API (for client systems)
Too expensive for production features in client systems. OpenAI models give better cost per intelligence.
Instead: OpenRouter, picking the model by cost per intelligence.
- Cloudflare D1
The business case is thin, and there's no tooling to migrate thousands of databases. Durable Objects do per-tenant SQLite better.
Instead: Durable Objects for per-tenant SQLite; Neon for a normal database.
- Cloudflare Hyperdrive
A Postgres connection pool for Workers only. My business logic isn't on Workers, so I don't need it.
Instead: Neon's HTTP driver, which works from Workers and Lambda alike.
- Cloudflare Queues Skip Proposed
Fine when producer and consumer are both Workers. Inside AWS, SQS wins.
Instead: SQS: FIFO, IAM and batched Lambda consumers, inside AWS where the work is.
- Cloudflare Workers AI Skip Proposed
The same open models cost about twice as much as on OpenRouter, and frontier models are capped at 20 requests a minute.
Instead: OpenRouter: the same open models for about half the price, and newer ones.
- Google Cloud
Good products, not enterprise-complete enough for my clients.
Instead: AWS, which covers more of what enterprise clients need.
- Netlify
An excellent platform, built around one workflow (push, preview, ship, roll back), and where many AI-built sites already live. I skip it because my clients are enterprises and need far more backend than a web platform covers.
Instead: Cloudflare static hosting for the front end, Lambda or Cloudflare Workers for the API, Neon for the data, all provisioned with Pulumi.
- Postmark
The best deliverability reputation, but no marketing platform, no idempotency and unsigned webhooks. I picked Resend.
Instead: Resend: transactional and marketing on one platform, with idempotency keys and signed webhooks.
- TypeScript
I don't use it. Plain JavaScript and strong tests.
Instead: Plain JavaScript on Node.js, with strong tests.
- Vercel
An excellent platform, and its one workflow (push, preview, ship, roll back) is the real product. I skip it because my clients are enterprises: most of their work happens in the backend, and I need all of AWS behind it.
Instead: Cloudflare static hosting for the front end, Lambda or Cloudflare Workers for the API, Neon for the data, all provisioned with Pulumi.
Gotchas, with fixes
91 things that bit me or my clients, each with the fix. This is the part nobody else writes down.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
Gotcha: Without a dead-letter queue, failed messages are deleted.
Fix: Always configure a DLQ.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.