skip to content

Renaming the prefix of 200,000 finished renders in an object store runs for hours and bills per operation — what is the store doing?

level: middleimportance: must knowfreq 62%

answer

  1. the namespace is flat
  2. a prefix is not a directory entry
  3. nothing to re-point, so keys are rewritten
  4. two requests per object, plus paging
  5. copy first, delete second, not atomic

basics

~20 s

There is no rename. The key namespace is flat, so the store lists every key under the old prefix and performs a copy to the new key followed by a delete of the old one — per object. Cost and duration scale with object count, not bytes.

solid answer

~40 s

A prefix is not a directory; it is the leading text of a flat key. Nothing in the store holds an entry you can re-point, so a "rename" expands into a paged listing plus, for every key found, a server-side copy to the new key and a delete of the old one. That is work proportional to the **number of objects**, which is why 200,000 small renders take hours while one enormous file would be quick. It is also **not atomic**: while the run is in flight, some renders answer under the old prefix, some under the new, and a crash leaves you straddling both. The lesson is to keep mutable grouping out of the key — put it in an index or a manifest object you can update with one write.

code

pseudocode · 14 lines
pseudocode
oldPrefix = "renders/q3/"
newPrefix = "renders/archive-q3/"
token = null

repeat
    page = store.listKeys(prefix: oldPrefix, after: token, max: 1000)

    for each key in page.keys
        suffix = key.withoutPrefix(oldPrefix)
        store.copyObject(from: key, to: newPrefix + suffix)   // request 1
        store.deleteObject(key)                               // request 2, only after the copy

    token = page.nextToken
until token is null

go deeper

for a junior

Remember that keys are one flat string and folders are a display convention. If someone asks you to rename a folder in an object store, your first thought should be that every key underneath it has to be written again.

for a middle

Explain the expansion precisely: a paged listing, then a copy and a delete per object, so the work scales with object count rather than with bytes. Say why the copy comes first.

for a senior

Show the operational judgment — the run is not atomic, so name what readers see mid-flight, how you make it resumable and idempotent, and how you dual-read until it finishes. Better still, say how you avoided needing it.

for a principal

The angle is schema ownership. Mutable grouping in a key is a design decision someone made in week one, and it becomes a recurring migration for every team that uses the store. Setting the keying standard is cheaper than paying the migration repeatedly.

## A prefix is text, not a directory An object store has one flat namespace of keys. `renders/q3/clip-482.mp4` is a single string; the slashes have no more meaning to the store than any other character. Tools and consoles render them as a tree because it is useful to look at, and listing APIs will even group results at a delimiter for you — but there is **no directory object**, so there is nothing to re-point. That has one hard consequence: the store cannot offer a cheap rename. In a filesystem, renaming a directory changes one entry and every descendant follows because descendants are reached *through* that entry. In a key-addressed store, every descendant key contains the old prefix in its own text, so every one of them has to be written again under a new string. ## What the run actually does For each object, a prefix rename is: 1. **A listing** — paged, so one request returns some bounded number of keys plus a continuation token you pass back to get the next page. 2. **A copy** — usually a server-side copy, so the bytes do not travel back to your machine, but it is still a request per object and the store still writes a new object. 3. **A delete** — a second request per object, and only after the copy is confirmed, unless you are willing to lose data on a crash. So the run is roughly two requests per object plus one per page, and its duration is set by how many of those you can have in flight at once. Two hundred thousand objects is hundreds of thousands of requests. Whether the objects are a kilobyte or a gigabyte barely moves the request count, which is why people are surprised: they reason about the bill in bytes and the store is charging them in operations. | | filesystem rename | object-store prefix rename | |---|---|---| | work done | one directory entry changed | copy plus delete, per object | | scales with | nothing, it is O(1) | the number of objects | | atomic | yes, for the subtree | no, object by object | | crash mid-way | nothing moved | some objects under each prefix | | bytes moved | none | rewritten, usually server-side | ## It is not atomic, and that is the sharper problem The cost is annoying; the lack of atomicity is what breaks production. During the run a reader that resolves a render's location by building the key from a prefix will find some renders under the old string and some under the new. If the job dies halfway, you are left straddling both and must be able to re-run it safely — which means the copy must be idempotent (copying over an existing identical key is harmless) and the delete must tolerate a key that is already gone. Order matters too: **copy first, delete second**. An interrupted run then leaves duplicates, which cost storage and can be cleaned up. The reverse order leaves holes, which cost data. ## What to do instead - **Do not encode mutable grouping into the key.** If the grouping can change — a customer name, a project, a pipeline stage, a date bucket you might re-cut — it does not belong in the key text. Key by something immutable, such as an identifier the object is born with. - **Keep the grouping in an index.** A database row or a manifest object maps grouping to key. Re-grouping a quarter of renders then becomes one write to the index, not 200,000 writes to the store. - **Where the keys must move, accept it and plan the run.** Parallelise across many key ranges, make the run resumable from the continuation token, keep the copy-then-delete order, and dual-read from both prefixes until the run has finished. - **Expect listings to be paged.** Any "find everything under this prefix" operation is a scan of keys in order, and on a very large store that is a long-running job in its own right — not something to do inside a request handler. ## The general lesson Three filesystem habits break against a key-addressed store, and this is the one that costs the most: **rename is free, listing is cheap, and a directory is a thing**. None of those hold here. Providers differ in the details — some expose a copy that also lets you change metadata, some offer batch operations that run the same loop on your behalf, and consistency of a listing immediately after a write is stronger on some platforms than others — but none of them make the old key vanish without a write against the new one.

  • Why does the loop copy before deleting rather than the other way round?
    So an interruption is survivable. Copy-then-delete leaves duplicates under both prefixes, which cost storage and are cleaned up on a re-run. Delete-then-copy leaves keys that exist under neither prefix, which is data loss. The same ordering makes the run idempotent: re-copying an identical key is harmless, and deleting an already-deleted key is a no-op you can ignore.
  • The renders are enormous, but the run is still slow. Why does object size barely help?
    Because the run is bounded by requests, not bytes. Each object costs a copy and a delete regardless of size, and the copy is usually executed inside the store rather than pulling bytes through your machine. Halving the object count would halve the run; halving the object size changes almost nothing.
  • How would you make this re-grouping cheap the next time it is asked for?
    Key each render by an immutable identifier it is born with, and hold the grouping in an index — a row in a database or a manifest object listing the members. Re-grouping is then a write to the index, and the keys never move. Readers resolve grouping through the index instead of constructing keys from a prefix.

saying these in an interview costs you the question

  • Says the store renames a folder in one atomic operation
  • Thinks a prefix is a real directory the store maintains
  • Expects cost to track bytes when it tracks object count
  • Deletes the old key before confirming the copy landed
  • Assumes a listing returns every key in one response
  • Builds keys from a grouping that changes every quarter