Naming a Recording File So It Sorts and Never Collides

Naming a Recording File So It Sorts and Never Collides

October 6, 2026

A recorder writes a file every time somebody presses stop. The name is decided in one line, usually early, usually without much thought — and then lives in a folder with three hundred siblings, gets sorted by a file manager, gets attached to an email, and gets uploaded alongside a file from another machine with the same name.

Four requirements, and they conflict just enough to be worth thinking about once.

Four filename date formats compared for sorting and readability
Four date formats in a filename, one of which works.

Sort order is the whole point

Files sort as text, so a date has to be written so that alphabetical order and chronological order are the same thing. That means year first, fixed width, zero-padded:

2026-10-06-143205        sorts correctly, always
06-10-2026-143205        groups every 6th of the month together
Oct-6-2026               sorts April, August, December, February…
1759751525               correct, and unreadable to a human

ISO 8601 is the only format that is both correct and readable. Every other arrangement optimises for one at the cost of the other.

The characters you cannot use

The set of forbidden characters is the union of every platform the file might reach, not just the one it was made on:

Windows forbids:  < > : " / \ | ? *
macOS forbids:    : and /
Linux forbids:    / and NUL

Union, for anything portable:  < > : " / \ | ? * and control characters

The colon is the trap. A natural-looking timestamp of 14:32:05 is fine on Linux, legal-but-displayed-oddly on macOS, and invalid on Windows — so a recording made on a Mac cannot be extracted from a zip on a colleague’s PC.

const safe = (s) => s.replace(/[<>:"/\\|?*\x00-\x1f]/g, '-');

Windows also reserves a short list of names regardless of extension: CON, PRN, AUX, NUL, COM1–COM9, LPT1–LPT9. If any part of the name is user-supplied, check against that list — a window titled “NUL” is unlikely and a user typing it is not.

Trailing dots and spaces disappear

Windows silently strips a trailing dot or space from a filename. A file saved as demo .mov becomes demo.mov, which breaks anything holding the original string:

name = name.replace(/[. ]+$/, '');

Making collisions impossible

Two recordings started in the same second produce the same timestamp, and so do two machines uploading to one folder. Seconds are not enough on their own.

Three approaches, with different trade-offs:

  • Add milliseconds. Cheap and still readable. Does not help across machines.
  • Add a short random suffix. Four hex characters makes a same-second collision vanishingly unlikely, at the cost of four meaningless characters.
  • A full UUID. Unique everywhere, unreadable, and tells a human nothing about which file this is.
// readable and effectively unique
`HappyRec-2026-10-06-143205-7f3a.mov`

For a recorder, the short suffix is the right compromise. A UUID filename is correct and makes a folder of recordings useless to browse, which is the thing people actually do with them.

Forbidden filename characters on each platform and their union
What to strip, which is more than the local platform requires.

Never check-then-write

The obvious uniqueness check is a race, and on a recorder it is a race you will lose eventually because two windows can stop at once:

// wrong: another process can create it between the check and the write
if (!fs.existsSync(path)) fs.writeFileSync(path, data);

Ask the filesystem to fail instead, and let it be the arbiter:

const fd = fs.openSync(path, 'wx');   // fails if it exists, atomically

On collision, do not retry with the same name plus a counter in a loop that re-checks — generate a new suffix and try again. Three attempts is plenty; if all three collide, something else is wrong.

Length limits that are shorter than you think

Most filesystems allow 255 bytes for a single name, but the full path is the real limit, and on Windows it is 260 characters unless long paths are explicitly enabled.

Where a name includes something user-supplied — a window title, a project name — truncate that part rather than the whole name, so the timestamp and the suffix survive:

const MAX_TITLE = 60;
const title = sanitise(windowTitle).slice(0, MAX_TITLE);
const name  = `HappyRec-${title}-${stamp}-${suffix}.mov`;

And truncate by characters that are whole, not by bytes. Cutting a multi-byte character in half produces a name that is invalid UTF-8, which some tools will reject and others will render as a replacement character forever.

Window titles are not safe input

Including the window title is genuinely useful — HappyRec-Checkout-flow-bug-... is far better than a bare timestamp. It is also the most dangerous part of the name.

A browser window title contains whatever page is open, which may be a customer’s name, an invoice number, or a search. A file sitting on the desktop with that in its name is a small privacy problem that nobody intended.

The defensible default is a timestamp only, with the title as an option the user turns on. If the title is included, sanitise it, truncate it, and strip anything that looks like an email address or a long digit string.

Three filename decisions covering collisions and privacy
Three decisions about the rest of the name.

Local time or UTC in the name

Inside the file, timestamps are UTC. In the filename, use local time — the name is read by a human who knows when they recorded something, and a file called 09-02-05 for a recording made at 2:32pm is confusing every time.

If files from several machines are collected centrally, include the offset so the ordering is still recoverable:

HappyRec-2026-10-06-143205+0530-7f3a.mov

The plus sign is legal on every platform; the colon inside an offset is not, which is why the compact form is the one to use.

Testing the names, cheaply

test('filename is portable and sorts chronologically', () => {
  const a = makeName(new Date('2026-10-06T09:02:05Z'));
  const b = makeName(new Date('2026-10-06T14:32:05Z'));
  expect([b, a].sort()).toEqual([a, b]);
  expect(a).not.toMatch(/[<>:"/\\|?*]/);
  expect(a.length).toBeLessThan(120);
});

Plus one that generates two names in the same millisecond and asserts they differ, which is the case the obvious implementation gets wrong.

A short answer

ISO date first, so text order is time order. Strip the union of every platform’s forbidden characters, and the trailing dots and spaces Windows removes silently. Add four hex characters rather than a UUID. Open with wx instead of checking first. Truncate the user-supplied part by whole characters, not bytes. Keep window titles out of the name by default. Local time in the name, UTC inside the file.