Skip to content
Back to Blog
Performance11 min read

Serverless Architecture Migration: 8 Gotchas [Solved]

Moving to serverless? Eight technical traps—cold starts, state, vendor lock-in, and more—plus the fixes that actually work in production.

Written by Abdul AbrorTechnical Hosting Support Engineer
Serverless Architecture Migration: 8 Gotchas [Solved]
On this page

I've walked three teams through serverless migrations in the last two years. Every one hit the same traps.

You promise your manager a 40% cost reduction and faster deploys. Then week two arrives: functions time out under load, your staging bill is triple what you predicted, and the database can't handle ten thousand concurrent connections. The platform works fine—you just skipped the boring parts.

Here are the eight gotchas that matter, with the fixes we actually used.

Cold starts will ruin your first launch

Your function sits idle for five minutes. The first request after that waits while the runtime spins up a container, loads your code, opens database connections, and initializes dependencies. That can be two seconds. For a checkout page, that's a bounce.

The problem gets worse at scale. Deploy a new version during traffic and every instance starts cold. A hundred users hit the site at once—fifty of them wait.

Keep critical paths warm

Scheduled pings work. A CloudWatch event hits your function every three minutes with a lightweight health-check payload. The function stays warm, skips the cold-start tax, and costs you almost nothing. For user-facing endpoints, this is the tax you pay for serverless benefits.

Package size matters more than you think. A 50 MB deployment with fifteen libraries takes longer to unpack than a 5 MB one. Strip dev dependencies, use Lambda layers for shared code, and compile native binaries for the target runtime. I've seen cold starts drop from 3 seconds to 800 milliseconds just by trimming the zip.

Provisioned concurrency solves the problem by keeping instances alive, but it defeats half the cost argument. Use it only for endpoints that must respond in under 500 milliseconds every time.

Stateful sessions break in ways you don't expect

Your monolith stored user sessions in memory or on disk. Each request hit the same server, so session data lived where you expected. Serverless functions are stateless by design—every invocation might land on a different instance.

Storing sessions in function memory fails immediately. Use an external session store: Redis, DynamoDB, or Memcached. Every function reads and writes session tokens to the shared layer.

The real problem is WebSockets. Your chat feature holds open a persistent connection; the function behind it must stay alive. API Gateway supports WebSocket routes, but connection state still lives externally. Store connection IDs in a table, route messages through SQS or EventBridge, and accept that you're building a distributed system now.

File uploads are another trap. Uploading a 200 MB video through a function will time out. Generate a presigned S3 URL, let the client upload directly, then trigger a function on the S3 event. Shorter path, no timeout, lower cost.

Database connection limits hit you in production

Your monolith opened ten database connections in a pool and reused them. Twenty app servers meant two hundred connections total. A thousand Lambda functions trying to connect simultaneously will exhaust your RDS limit in seconds.

Traditional connection pooling doesn't work. Functions scale faster than your database can accept connections, and tearing down unused connections takes too long. You'll see FATAL: remaining connection slots are reserved errors under any real load.

Use a proxy layer

RDS Proxy sits between your functions and the database, pooling connections and multiplexing requests. A thousand functions share fifty real connections. Failover and retries come included. It costs a few dollars a month and solves the problem completely.

For Postgres or MySQL outside AWS, PgBouncer or ProxySQL work the same way. Run them on a small instance and point all functions at the proxy. Connection limits stop being a crisis.

HTTP-based databases like Fauna or PlanetScale skip the problem entirely—they were built for serverless workloads. If you're starting fresh, that's the easiest path.

Timeouts are hard limits, not suggestions

AWS Lambda functions time out at fifteen minutes maximum. Azure caps at ten. A batch job that takes twenty minutes in your current stack won't finish. The platform kills it mid-run.

Break long tasks into smaller chunks. Instead of one function processing five thousand records, use Step Functions or a queue to process batches of fifty. Each invocation finishes in seconds, and the entire job scales horizontally.

Some workloads genuinely don't fit serverless. Video transcoding, large ETL jobs, and ML training runs belong on ECS, Fargate, or a dedicated instance. Trying to force them into functions wastes your time and costs more.

Vendor lock-in is real but manageable

You write Lambda-specific code using API Gateway events and DynamoDB clients. Six months later, you want to move to Google Cloud and realize every handler is AWS-shaped.

Abstraction layers help. Wrap platform-specific calls in your own interfaces—StorageService, QueueService, EventBus—and swap implementations per environment. The initial work is boring, but migrating later becomes possible instead of impossible.

Use frameworks built for portability. The Serverless Framework, SAM, and Terraform describe infrastructure as code; changing providers means editing YAML, not rewriting handlers. Avoid writing raw CloudFormation unless you're committed to AWS long-term.

The honest answer: most teams stay on their first platform. Lock-in matters less if the platform does what you need. Optimize for velocity now, not theoretical migrations later.

Cost surprises come from traffic you forgot about

Your estimate assumed ten thousand requests per day. You forgot about health checks, webhooks, retries, and that cron job the intern added last year. Now you're paying for two million invocations monthly.

Audit everything that triggers a function

Log every invocation source for a week. You'll find monitoring tools hitting endpoints every thirty seconds, third-party services sending redundant webhooks, and internal jobs running more often than they need to. Batch those calls, disable verbose health checks, or move them to a cheaper tier.

Smaller functions cost less. A 128 MB function is eight times cheaper per millisecond than a 1 GB one. Trim memory unless profiling shows you need it. Most CRUD endpoints run fine on 256 MB.

Execution time matters more than invocation count. A function that runs for ten seconds costs fifty times more than one that finishes in 200 milliseconds. Profile slow paths, cache expensive queries, and precompute what you can.

Local development becomes a puzzle

You used to run the entire app on your laptop. With serverless, the app is twenty functions, three queues, an API gateway, and two databases. Running all of that locally is painful.

The SAM CLI and Serverless Offline simulate the runtime environment well enough for most work. You can invoke functions, hit local endpoints, and debug in your IDE. They're not perfect—IAM policies and service integrations don't behave identically—but they're close.

Some teams skip local emulation entirely and test against a personal dev account in the cloud. Deploy on every save, test against real services, tear down after the session. Fast feedback loops matter more than perfect local parity.

Integration tests become more important than unit tests. Your function works; the question is whether it handles the event format, calls the right downstream service, and retries failures correctly. Mock less, test the real flow more.

Observability gaps hide failures

Your monolith logged everything to a file you could grep. Serverless functions scatter logs across CloudWatch groups, execution traces through X-Ray, and metrics into three dashboards. Finding why a request failed takes ten minutes instead of ten seconds.

Centralize logs immediately

Ship all function logs to a single destination—Datadog, Honeycomb, CloudWatch Insights, or even an S3 bucket you can query with Athena. Searching one place beats jumping between fifteen log groups.

Structured logging is non-negotiable. JSON logs with request IDs, user IDs, and function names let you trace a request across five functions in seconds. Use a library, set a standard schema, and enforce it in code review.

Distributed tracing shows you the full path. X-Ray or OpenTelemetry instruments every hop—API Gateway to Lambda to DynamoDB to another Lambda—and surfaces the slow step. Guessing where latency lives is over.

Set alarms on error rates and latency percentiles, not just averages. A P50 latency of 200 milliseconds looks great until you see the P99 at 8 seconds. Functions fail silently when retries exhaust; alerts catch what logs miss.

What works in practice

Serverless migration is a rewrite in disguise. You're not lifting and shifting—you're rethinking how the app works. That's harder than the sales pitch suggests, but the result scales better and costs less if you avoid the traps.

Start with the edges: a new API endpoint, a background job, or a webhook handler. Learn the platform on something small before migrating your billing system. Measure everything—latency, cost, error rate—and compare it to your old stack honestly.

The teams that succeed treat serverless as a tool, not a religion. Some workloads belong on functions; others don't. Mixing architectures is fine. The goal is a system that works, not perfect adherence to a pattern.

FAQ

Can I migrate a monolith to serverless all at once?
No. Incremental migration through an API gateway or event bus lets you move one feature at a time while the monolith handles the rest.

What's the biggest cost mistake?
Not monitoring invocation counts and execution time from day one. Small inefficiencies compound fast at scale.

Do I need a service mesh for serverless?
No. Functions are already isolated and independently deployable; a mesh adds complexity without much benefit in this context.

How do I handle long-running jobs?
Use Step Functions, SQS queues, or container-based services like ECS for anything over ten minutes. Functions weren't built for that.

Where to focus first

Fix cold starts and database connections before you ship to production. Everything else you can patch later. Logs and alarms go live on day one, not after the first outage.

Test at scale in a staging environment that mirrors production traffic. Load testing a serverless app is cheaper than debugging it under real user load. Budget for the experiments.

Migration is a process, not a weekend. Pace it, measure it, and don't let perfection stop you from shipping working code.