Document your units (or production will do it for you)
I was pairing with the engineers at Nimble Ape on a phone system. Someone shared a perfectly reasonable example configuration:
QUEUE_TIMEOUT=10
One person read that as 10 seconds. Someone else read it as 10 milliseconds.
Neither interpretation is ridiculous. The variable name simply didn’t say. But the system definitely cared. A timeout that is 1,000x smaller than intended can turn queue processing into a weird little machine for shedding work.
This is one of those bugs you only need to hit once.
This post is the smallest set of habits I’ve found that prevents it: (1) make the unit part of the identifier anywhere the number “lives”, and (2) when it’s worth it, promote that primitive into a value object at the boundary so the rest of the codebase can’t accidentally forget what it means.
The day 10 meant two different things
The awkward bit about unit bugs is that everybody is behaving sensibly.
- A developer sees
timeout = 10and assumes seconds because most human-facing time is in seconds. - Another developer assumes milliseconds because many APIs accept milliseconds for precision.
Both are right often enough that nobody feels the need to ask…until you deploy.
If you’re thinking “surely we’d notice in code review”, you’re not wrong. You probably would. But config tends to travel: it gets copied into README snippets, issue comments, wiki pages, dashboards, incident docs, and then back into code somewhere else. Comments don’t follow it. Names do.
Make the unit part of the name (where the number lives)
My default rule: unitless numbers in config are a bug.
So in this case, the fix is not a comment. It’s renaming the variable:
# Before
QUEUE_TIMEOUT=10
# After (explicit)
QUEUE_TIMEOUT_MS=10
You can do the same for distances and sizes:
MAX_UPLOAD_BYTESAVATAR_SIZE_PXGEO_RADIUS_MPAPER_WIDTH_MM
For databases: same idea. Prefer timeout_ms or timeout_seconds over timeout. If you can, add constraints so the database enforces the contract too.
The important detail is “where the number lives”. A comment in the code that reads the config is better than nothing, but it fails the moment somebody uses the value in a different code path.
Pick a canonical unit
Once you decide a unit, stick to it.
For operational timeouts (queue timeouts, retries, HTTP timeouts, TTLs), milliseconds or seconds are the two sane options. Pick one as your canonical storage unit and make conversion explicit at the edges.
My bias:
- Store milliseconds when you routinely deal with sub-second timings or you integrate with APIs that speak ms.
- Store seconds when humans read and set the value often and you don’t need sub-second precision.
Either is fine. What’s not fine is letting the unit be “whatever the last person assumed”.
Why this isn’t pedantry: Mars lost an orbiter to units
If you ever need ammunition for “yes, we really should care about this”, there’s the Mars Climate Orbiter.
The Mishap Investigation Board report describes a mismatch between what was specified (impulse in newton-seconds) and what a piece of ground software output (pound-force seconds). The navigation software underestimated the effect of thruster firings by a factor of 4.45, and the spacecraft was lost after it approached Mars far lower than intended. The report calls out the root cause as “failure to use metric units in the coding of a ground software file”.
Your queue timeout is not a spacecraft. But it is an interface. Units are part of that interface.
Laravel example: env var with unit, read via config
I’m going to translate the original incident (it wasn’t Laravel) into a Laravel-shaped example because the pattern is common.
In Laravel you should only read env() inside config files, because config caching means .env isn’t loaded during requests and env() will only return external system-level vars (Laravel configuration docs).
So wire the env var into a config file:
<?php
// config/telephony.php
return [
// Stored in milliseconds.
'queue_timeout_ms' => (int) env('QUEUE_TIMEOUT_MS', 10_000),
];
Then in application code, only use config():
$timeoutMs = config('telephony.queue_timeout_ms');
At this point you’ve already done the most important thing: the unit is attached to the name at the source of truth.
When it makes sense: promote to a value object
If the value is used in multiple places (or you do any maths with it), I like to convert it once at the boundary and then pass around a type that can’t be “milliseconds except when it isn’t”.
That’s a value object: a small object whose equality is based on its contents, not identity (Martin Fowler’s definition).
Here’s a minimal Duration for PHP 8.5 that stores a canonical unit (milliseconds), gives you explicit factories, and enforces an invariant (no negatives):
<?php
declare(strict_types=1);
namespace App\Support;
use Carbon\CarbonInterval;
use InvalidArgumentException;
final readonly class Duration
{
private function __construct(public int $milliseconds)
{
if ($milliseconds < 0) {
throw new InvalidArgumentException('Duration cannot be negative.');
}
}
public static function milliseconds(int $milliseconds): self
{
return new self($milliseconds);
}
public static function seconds(int $seconds): self
{
return new self($seconds * 1000);
}
public function toCarbonInterval(): CarbonInterval
{
return CarbonInterval::milliseconds($this->milliseconds);
}
public function toSecondsFloat(): float
{
return $this->milliseconds / 1000;
}
}
Usage stays obvious, even when you move code around:
use App\Support\Duration;
$timeout = Duration::milliseconds((int) config('telephony.queue_timeout_ms'));
// Example: if a library expects seconds as a float:
$client->withTimeoutSeconds($timeout->toSecondsFloat());
That last line is doing something quietly important: the conversion is explicit, close to where you need it, and easy to code review.
If you want to go even further, you can ban Duration’s constructor entirely (keep it private like above) so the only way to make one is Duration::milliseconds() / Duration::seconds(). That’s how you stop “naked integers” spreading.
A quick unit smell test (for existing systems)
If you want to find the landmines without a full refactor, search for:
- Config keys like
TIMEOUT,TTL,RETRY,DELAY,INTERVAL,BACKOFFwith no_MS/_SECONDSetc. - Database columns named
timeout,duration,interval,size,radiuswith no unit. - Code that multiplies or divides by 1,000 (often a sign you’re converting ad-hoc).
- Places where humans frequently copy/paste values (docs, runbooks, dashboards).
Fix the names first. Types can come later.
Wrap-up
Documenting units isn’t bureaucracy, it’s reliability work.
When a number lives in config or a database, make the unit part of the identifier (QUEUE_TIMEOUT_MS, timeout_seconds, max_upload_bytes). When that value starts flowing through more than one place, convert it at the boundary into a value object (Duration, Distance, Size) so the rest of your codebase can’t “forget” what it means.
Your future self (and your on-call rota) will thank you.