Skip to content

Field notes

RSS

Things I figured out, got wrong, or want to remember. Too small for a post, too specific to lose. Newest first, all the way down.

2026

Ten thousand steps, give or take four thousand

Set up Step Up App on a whim. Five of us in it now, one on Android, so it had to run on both.

I’d tell you ten thousand a day. My average is six, and the app shows it to four other people, which is the only reason I know.

One of them is past four hundred and twenty thousand steps for September with nine days still to go. That’s twenty thousand a day, every day. I’m not catching him.

A homelab is a very expensive way to learn that DNS works

There is a particular kind of satisfaction in making an old computer do something useful.

Then you add Caddy. Cloudflare. Tailscale. Jellyfin. Monitoring. Containers. Another container to manage the containers.

Eventually you’re spending an evening fixing a problem that wouldn’t exist if you had just paid for the service.

That’s part of the point.

A fast website is mostly a website that doesn't do much

I’ve spent an unreasonable amount of time looking at why some personal websites feel instantaneous.

The answer is less exotic than I expected.

Don’t ship what you don’t need. Don’t make the browser assemble what could have been a document. Don’t add JavaScript just because the framework makes it easy.

There is something satisfying about a website that arrives almost completely finished.

Debian doesn't install sudo if you set a root password

sudo: command not found on a fresh install isn’t a broken image. If you set a root password during setup, Debian assumes you’ll use su and skips the package entirely. Leave that field blank and it installs sudo and adds you to the group for you.

su -
apt update && apt install sudo -y
usermod -aG sudo your_username
exit

Log out fully and back in. Group membership is read at login, so a new shell in the same session still won’t see it. Once you’re back in, sudo whoami prints root.

This surprised me because I’d assumed sudo was a base package everywhere. It isn’t. It’s a package Debian installs conditionally, based on an answer you gave several minutes earlier and have already forgotten.

Read the standard library before reaching for a package

I spent an afternoon evaluating rate limiter packages before someone pointed at golang.org/x/time/rate, which does exactly what I needed and is about two hundred lines I could actually read end to end.

This keeps happening. Go’s standard library and the x/ repos cover more than I expect, and the code is short enough to read rather than trust. Now the first step is a search there, and only then in the wider ecosystem.

Write the migration so it can run twice

Deploys get retried. Pods restart mid-migration. Someone runs the command again because they weren’t sure it took.

IF NOT EXISTS on creates, IF EXISTS on drops, and a backfill written as an idempotent update rather than an insert. It costs nothing to write it this way the first time and saves an incident later.

The exception is anything destructive. Those I want to fail loudly on a second run, because a DROP COLUMN that quietly succeeds twice means I’ve lost track of what state the database is in.

Reading a book is not the same as finishing one

I’ve started thinking less about how many books I finish and more about whether a book changes what I notice.

A book can sit half-read for months and still give you an idea you carry around for years.

Finishing is a useful metric for Goodreads.

It isn’t necessarily a useful metric for reading.

2025

The first kilometre is supposed to feel bad

Two years of assuming a rough opening kilometre meant a rough run. It doesn’t. It means I haven’t warmed up yet, which is the expected state of a body that was asleep twenty minutes ago.

Starting deliberately slower than feels right makes kilometres three through eight better than they’ve ever been. The same lesson keeps showing up at work, where the instinct to start fast is also usually wrong.

The goroutine leak was a missing cancel

Memory climbing slowly over days, nothing obvious in the heap profile. It was in the goroutine profile instead: thousands of them parked on a channel receive that would never fire.

ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel() // this line was the whole bug

WithTimeout allocates a timer and registers the child with its parent. Without cancel(), neither is released until the timeout fires — and if the parent outlives the process, effectively never. go vet catches the obvious cases; this one was behind a helper function, so it didn’t.

Paid for the year so I'd stop deciding

Bought twelve months of a training-plan app in one payment, which is not how I usually buy software. Monthly is the flexible option, and flexible is exactly the problem; it leaves a small decision sitting there every thirty days.

Prepaying removes it. There’s no renewal to reconsider in April when it’s raining and I’ve already skipped a week. The money is gone, and the only thing left to decide is whether to run.

Priced against one evening out, a year of structured training isn’t a close call. Two runs in, which is far too early to say anything about the plans themselves.

2023

One SSH key, one GitHub account

Pushed to a repository I definitely had access to and was told I didn’t. The key was the problem: GitHub only lets a key be registered once across all accounts, and mine was still attached to the other one.

Nothing in the error says that. You get a message about the repository, so you go and check the repository, and everything there is fine.

ssh -T git@github.com

The reply names the account it just authenticated you as. When that’s the wrong name, stop looking at permissions.