November 7, 2026
From PHP to Go: Rebuilding the Backend for HittaPodd.se
How HittaPodd.se moved from PHP to Go and from a shared database to one of its own. What went well, what went wrong, and what we learned.
HittaPodd.se now runs on Go. The old PHP API has been retired, and the app has a database of its own instead of sharing one with other things.
This isn't a how-to. I'm leaving the code out and writing about why we did it, what helped, and the things that didn't go as planned. We got a fair amount wrong along the way.
Why we did it
Nothing was broken. The PHP version worked. The reason is bigger than one app: we want to retire PHP as a runtime across our product portfolio and move toward Go. HittaPodd was the next one in line.
Fewer runtimes means less to patch, less to keep in your head, and less to explain to anyone joining a project. That was the bet, at least.
There was also a reason specific to HittaPodd. The app shared a database with other things. Nobody decided that, it just happened over the years. It was convenient at first and then turned into a dependency nobody had chosen. Changing runtimes seemed like a reasonable time to deal with it.
How it went
It wasn't done in one go. A first pass in March got the basics and the background jobs running in Go: importing podcasts, reading feeds, making thumbnails, finding similar podcasts. Then it sat for a while because other work came first. We picked it up again in the autumn, and the actual cutover happened over a few weeks in late October.
Looking back, the pause helped. When we returned to it, it was clearer what was still missing.
Moving one piece at a time
We used nginx, which already sat in front of the API, to decide where each request went. PHP kept answering everything, and we moved groups of routes to Go one step at a time. First the catalogue (categories, collections, authors), then podcasts, episodes and search, then similar podcasts and series. The background jobs moved in between.
This worked better than I expected:
- Each step was small. A few routes at a time meant that when something looked wrong, there wasn't much to search through.
- Going back was easy. Sending a route back to PHP was a change in nginx, not a new release. That made it less scary to try the next step.
- We could see what was left. Whatever nginx still sent to PHP was the work that remained.
- PHP and Go ran side by side. We could compare them on real traffic, instead of guessing how they'd differ.
What went better
Speed. Search was the slowest part. Once we measured it on production data, we got an uncached episode search from about 1.4 seconds down to about 0.6, and later made most searches faster again. Day to day, the API answers with a median around 18 ms, though that's from a single hour, so take it as a rough picture.
The database is ours. HittaPodd now has a database that only holds HittaPodd data, and Go owns the schema. Migrations live next to the code and run in CI. I can change a table without first wondering who else reads it.
A reason to revisit the background work. The feed updater now checks feeds less often when they rarely change and backs off when one misbehaves. Thumbnails are made from a queue. Similar podcasts are found through shared vocabulary rather than name matching. None of that required Go, but the rewrite gave us a reason to look at it.
Room for new things. With the foundation in place, we've been able to add accounts, playlists, OPML import, an admin, and an MCP server. We probably wouldn't have gotten to those on the old setup.
What was hard
Untangling the shared database. Working out what actually belonged to HittaPodd took longer than the code did. After years with other tenants, the boundaries were blurry. We started from an exact copy of production's schema and sorted it out from there. I'm not certain we made every call right the first time.
Matching the old behavior. The frontend relied on details that only the PHP code defined, and nobody had written them down. In one case a renamed field would have made every private collection look public. We added tests that compare the API spec to the real code, and they found 87 mismatches the first time they ran. That was humbling.
The first route move broke the site. We had a new frontend ready, but its deploy failed, so the live site was still running an older build. Installed apps and open tabs do the same. They kept expecting PHP's response format, and Go answered in a different one. We should have caught that before moving anything. The fix was to let Go answer in the old format when asked. nginx asks on behalf of old clients, and new clients get the new format. We used that for every step after.
Moving the data. Data migrations make me nervous. We rehearsed it several times, ran it, checked the result, threw it away, and repeated, until the real run was uneventful.
Real traffic is different from tests. Some of our heaviest traffic comes from our own apps and partner sites, and it doesn't behave like anything we'd have made up. Search was the clearest case, and we only found it by looking at real numbers.
What we'd do the same, and what we'd change
Things we'd repeat:
- Move one route at a time behind a switchboard, even though it feels slow.
- Get the old behavior right first and improve things afterwards.
- Measure before tuning anything.
- Give an app its own database.
Things we'd do differently:
- Check what old clients expect before moving the first route.
- Write down the quirks of the old system earlier, before they turn into surprises.
Where that leaves us
HittaPodd.se is simpler to run now, and it doesn't depend on anything it didn't choose. It's also one less PHP app in the portfolio, which was the goal. There are surely rough edges we haven't found yet.
If you spot something that feels off on HittaPodd.se, I'd like to hear about it.
And if you have an old system you're nervous about touching, get in touch. This is the kind of work I like helping with.