July 16, 2026

The "temporary" migration script that ran in production for three years

Listen to the summary
0:00 / 0:00
The temporary migration script that ran for three years, cover graphic for erkshitiz.com.np

I found it while grepping the crontab on a server I was migrating off of, a line running a script called backfill_user_regions_TEMP.py every night at 3am. Not _temp as in “still figuring out the name,” _TEMP as in someone typed that in capitals on purpose, as a promise to their future self that this would not be a permanent fixture. Git blame put the commit at just under three years old. The script was still doing exactly what it was written to do: filling in a region column for user rows that predated the feature, one column, one query, running nightly against a table that had needed a one-time backfill three years earlier and, as far as anyone could tell, hadn’t needed it in at least two of them.

The thing that makes “temporary” a lie you tell yourself honestly

Nobody who writes a script like this is being dishonest. When you write it, “temporary” is completely true, you genuinely intend to run it once, confirm the backfill worked, and delete it that afternoon or that week. The dishonesty, if there is any, isn’t in the intent, it’s in the implicit bet the intent is making: that you, specifically, will have both the memory and the calendar space to close the loop later. That bet loses more often than people budget for, because the moment the script runs successfully and stops actively bothering anyone, it stops competing for attention against literally anything else on your plate. A broken thing gets fixed because it’s loud. A one-off script that quietly works forever has no mechanism that ever makes it loud again.

Crontab is an especially good trap for this because it’s designed to make things silent by default. A script scheduled once needs zero further human attention to keep running, correctly, for years, which is exactly the property you want from real recurring infrastructure and exactly the property that turns a scratch script into an ownerless permanent fixture without anyone deciding that on purpose.

What actually happened when it finally got questioned

The reason I found it at all was a server migration, and the reason it took real effort to retire, rather than being a two-minute delete, was that after three years nobody remaining on the team had written it, remembered what “region” was originally backfilling for, or could say with confidence whether some other since-built feature had quietly come to depend on that column being kept current by this nightly run instead of by whatever process was supposed to set it going forward. Deleting a temporary script that’s been running for three years isn’t deleting temporary code anymore, it’s deprecating an undocumented, unowned piece of production infrastructure, and that’s a fundamentally more careful job: check for readers, check for a service-level dependency, communicate a deprecation window, the whole ceremony you’d apply to retiring an actual internal service, applied here to something that was never supposed to still exist to retire.

That gap, between how much scrutiny code gets when it’s created versus how much scrutiny it needs by the time someone finally goes to remove it, is the actual cost of “temporary” code that overstays. It isn’t that the script was badly written, it did its one job correctly for three years without incident. It’s that the removal cost had compounded quietly the entire time, and nobody had been paying it down, because nothing forced anyone to look at it again.

The two habits that would have caught this

Looking back, there were two specific, cheap habits that would have caught this well before year three, and neither of them requires trusting future-you to remember anything.

Put an expiry in the artifact itself, not in your head. A comment at the top of the script, or better, a check the script runs against itself: today’s date compared against a hardcoded “delete after” date, logging a loud warning (or refusing to run) past that point. This converts “I’ll remember to delete this” from a fact about your memory, which degrades, into a fact about the code, which doesn’t. It doesn’t even need to be enforced automatically to work, a script that prints WARNING: this was supposed to be deleted 2024-03-01, someone should look at why it's still here to its own log output is enough to turn an invisible problem back into a loud one the next time anyone reads that log for any reason.

Never let genuinely temporary code enter genuinely permanent scheduling. A cron entry, a scheduled Lambda, a recurring CI job, all of these are permanent-by-default mechanisms, the same property that makes them good for real recurring work makes them bad homes for anything you expect to delete soon. If a backfill needs to run more than once to confirm it worked, that’s a signal to run it by hand those extra times, watched, rather than reaching for the scheduler as a convenience. The scheduler should be reserved for things you’ve already decided are permanent; using it for something you haven’t decided that about yet is how the decision gets made by default, silently, on your behalf.

Neither habit is more than a few minutes of extra effort at write time. Both are a lot cheaper than a “why does this table have a column three people currently at the company have never heard of, being written by a script two people currently at the company have never heard of” conversation three years later.