Technology

My personal website, like Lazarus from the grave

I ran through my free AWS credits, so I rebuilt the entire stack on free tiers instead: Vercel, Neon, Resend, Upstash. Here's what actually moved.

By Connor ShieldsMay 5, 2026(edited Aug 8, 2026)4 min read

This site was dead for about a year. Not slow, not degraded: dead. The domain lapsed. The AWS credits ran out around the same time, and neither one felt urgent enough on its own to actually deal with, so I just didn't, for the better part of a year. This is the post about finally getting around to it, and about not building it back the same way, since "the same way" is what let it die quietly in the first place without anyone, including me, noticing for months.

It used to run on AWS: EC2 for hosting, RDS for the database, S3 for files, SES for email. Once the credits ran out, the bill went from nothing to $60 a month for a site with a handful of visitors, most of them me. So this time I rebuilt the infrastructure on a stack that's actually free at this site's real traffic, one that can't quietly rack up a bill or need renewing behind my back, and moved everything over.

Not a redesign, a migration

Nothing about how the site works changed. Same features, same database schema, same code apart from what each new provider's SDK required. This was purely about not paying Amazon rent on a project that makes me no money.

What moved where

ComponentBefore (AWS)After
HostingEC2Vercel (free tier)
DatabaseRDS (PostgreSQL)Neon (free PostgreSQL)
File storageS3Vercel Blob (free tier)
EmailSESResend (free: 3,000/month)
Rate limitingIn-memoryUpstash Redis (free tier)
LoggingWinston, file-basedStructured JSON, per-request context

The rate limiting row is the one that actually needed fixing, not just moving. It was in-memory on EC2, which works fine on a server that stays running, and breaks completely on serverless, where every request can hit a cold instance with no memory of the last one. Upstash Redis fixes that by moving the counter somewhere that persists between invocations.

The constraint EC2 never had

EC2 doesn't care how long a request takes. Vercel's free tier does: 60 seconds, hard stop. The nutrient tracker takes a plain-text food description and calls GPT-4o-mini to extract the nutritional breakdown, and that call used to just happen inline: submit, wait, get a result. Fine on a box I controlled, occasionally not fine on serverless, where an AI call that runs long doesn't get a slow response: it gets killed.

That logic had to move into an actual queue. The submit endpoint now writes a row with status PENDING and returns immediately; the entry gets processed in the background and flips through PROCESSING to COMPLETED (or FAILED, with a retry) as it goes. The frontend polls a status endpoint every 3 seconds and shows a small queue panel, spinner for pending, clock icon for processing, until every entry it's watching comes back done. Nobody's request blocks on the AI call anymore. The page just checks back in a few seconds later.

What's worth knowing about the free tiers

Free tier limits actually in play

ServiceLimit
Vercel Blob500MB storage, 1GB bandwidth/month
Neon0.5GB storage, 190 compute hours/month
Resend3,000 emails/month

None of them have been a problem yet. If this site ever gets popular enough that they become a problem, that would itself be a good problem to have.

The old code still exists

The pre-migration codebase is sitting untouched on the aws-legacy branch, mostly so I never have to remember exactly what I changed: git remembers for me.

You're reading this on the result. If the site loads, it worked.

About the Author

Connor Shields

Written by Connor Shields