What "Getting Started with Serverless" Leaves Out
Every introduction to serverless opens the same way, and the opening is true. Here is a function:
export const handler = async (event) => {
const name = event.queryStringParameters?.name || 'World';
return {
statusCode: 200,
body: JSON.stringify({ message: `Hello, ${name}!` }),
};
};
Here is the infrastructure for it:
new sst.aws.Function("HelloWorld", {
handler: "src/hello.handler",
url: true,
});
Deploy, and you have a globally reachable HTTPS endpoint that scales from zero to thousands of concurrent requests and costs nothing while idle. No server, no capacity plan, no patching. That really is the whole thing, and it really does take about four minutes.
What the tutorial cannot tell you is that almost none of your remaining time will be spent there. It will be spent on three things it did not mention.
1. IAM is the actual interface, and it fails at runtime
The handler above touches no AWS services, which is why it works. The moment it calls one, the interesting part starts.
This site has exactly one backend route — a contact form that sends mail through SES. The code is unremarkable:
const ses = new SESClient({});
await ses.send(new SendEmailCommand({ /* ... */ }));
The part that makes it work is not in that file at all. It is in the infrastructure config:
new sst.aws.Astro("Portfolio", {
permissions: [
{ actions: ["ses:SendEmail"], resources: ["*"] },
],
});
Without that block the code is identical, the types check, the build passes, and the deploy succeeds. Then a visitor submits the form and gets a 500, because the function’s execution role has no ses:SendEmail.
This is the failure mode to internalise. In a traditional deployment, missing credentials tend to break at startup and you learn immediately. In serverless the permission is a property of a role, evaluated per call — so the gap between “wrong” and “observably wrong” is however long it takes for someone to exercise that path in production. Every new AWS call from a handler needs a matching entry, and nothing in your toolchain will remind you.
The corollary is that IAM stops being an ops concern you inherit and becomes part of the feature you are writing. Add a DynamoDB read on Tuesday without adding the policy, and you shipped a route that 500s on Tuesday.
2. Service sandboxes shape your code, not just your account
AWS puts new accounts into restricted modes, and the restrictions are not always a toggle you flip later. SES is the sharpest example, because it reaches into application design.
In the SES sandbox, both ends of the message must be verified identities. Not just the sender — the recipient too. That one rule is why the contact form on this site looks the way it does:
const SOURCE_EMAIL = 'bbobadillapisero@gmail.com';
new SendEmailCommand({
Source: SOURCE_EMAIL,
Destination: { ToAddresses: [SOURCE_EMAIL] }, // to myself
ReplyToAddresses: [visitorEmail], // never needs verifying
});
Sender and recipient are the same hardcoded address. The visitor’s address can never go in ToAddresses — it is not verified and never will be — so it goes in ReplyToAddresses, which the sandbox does not police. Hitting reply in the mail client then does the obvious thing.
That is not a shortcut. It is the only shape this feature can have until the account gets SES production access, and reading it as untidy code that ought to be parameterised would break the form. Quotas come along with it: 200 messages a day, one per second.
The debugging lesson is sharper still. When SES rejects a send, you get this:
MessageRejected: Email address is not verified.
It looks like a permissions problem and is not one. It means the identity is missing or was removed, while the IAM grant can be perfectly intact. Two unrelated failures, one indistinguishable symptom — and you will check the wrong one first. Verify the identity before touching the policy:
$ aws ses list-identities --region us-east-1
Region matters there too. Identities are per-region, and verifying in the wrong one produces exactly the same error message.
3. Cold starts are worst where the marketing is loudest
“Scales to zero” and “pay only for what you use” are the headline benefits, and both are real. They are also the same physical property as the thing that never makes it onto a slide: when traffic is low, your function is almost always cold.
A personal site is the extreme case. Visits are sparse enough that a large share of them pay full initialisation — runtime boot, module loading, SDK client construction — before your code renders a byte. The workload the economics flatter most is the workload the latency punishes most.
The response is not to fight the cold start. It is to keep requests away from the Lambda entirely. This site runs Astro in server mode, but every route that does not need a live request opts out:
export const prerender = true;
The whole blog is built to static HTML and served from CloudFront. The Lambda handles what is genuinely dynamic — the contact endpoint, and a robots.txt that varies by host — and nothing else. Most visitors never invoke a function at all.
That decision has consequences of its own, and they are not free either: prerendered routes and server-rendered routes end up on different origins behind the same domain, and the boundary between them is visible in production if you know where to look. That is its own post.
So when is it actually the wrong choice?
The usual list is correct and worth keeping:
- Long-running work — Lambda caps at 15 minutes, and it does not negotiate.
- Stateful applications — every invocation starts fresh; anything you need to remember lives somewhere else, and that somewhere else is now a second thing to operate.
- Predictable, constant load — a machine that is busy all day is cheaper as a machine.
The honest addition is the one this post is about. Serverless does not reduce how much infrastructure you have to understand. It changes which parts you meet, and when. You stop patching kernels and start reasoning about execution roles, per-service sandboxes, regional identity scoping, and which of your two origins answered a given request. That trade is worth making — I would not build this site any other way — but it is a trade, not a removal.
The hello world is honest about the model. It is quietly dishonest about the work.