Tutorial

How to Clean Up Orphaned Uploads

Clean up orphaned upload artifacts with a scheduled job that checks the age and DB linkage, so abandoned partials do not fill the disk.

On this page

  1. Introduction
  2. List old artifacts
  3. Check DB linkage
  4. Delete orphans
  5. Log the cleanup
  6. Run on a schedule
  7. Test the job
  8. Common pitfalls
  9. Conclusion

Introduction

The hardest part of remove abandoned uploads is not writing the code — it is deciding what the code is allowed to reject. A cleanup job setup that accepts everything is easy to ship and expensive to operate, because every malformed input becomes a production surprise. The discipline is to enumerate the shapes the system actually receives, encode them in cleanup job, and let the library reject the rest loudly. This keeps the happy path fast and the failure path visible.

The cost of remove abandoned uploads without cleanup job is paid in incidents, not in code. The code looks fine because the happy path works, and the happy path is what you demo. The failure path is what you operate, and the failure path is where cleanup job earns its keep. Instrument the rejections, alert on the spikes, and let the metric tell you when the guard is misaligned with reality.

The sections below walk through the concrete decisions: where to put the cleanup job check, how to keep it fast under load, what to do when the contract changes, and how to test the failure paths so they degrade on purpose instead of by accident.

List old artifacts

List old artifacts is the first decision because it determines what the rest of the cleanup job pipeline can assume. A wrong choice here turns into remove abandoned uploads under load, and the invariant you thought cleanup job guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for List old artifacts is the one most teams skip: drive cleanup job to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

async function cleanup(dir, maxAge) {
  for (const f of await fs.readdir(dir)) {
    const stat = await fs.stat(join(dir, f));
    if (Date.now() - stat.mtimeMs > maxAge && !await db.linked(f))
      await fs.unlink(join(dir, f));
  }
}

The javascript above is the shape to ship for remove abandoned uploads. It is small enough to audit and specific enough that the failure behavior is obvious. Adapt the constants to your traffic and latency budget, and resist the urge to handle cases the system has not seen yet.

Check DB linkage

The mechanism behind Check DB linkage is straightforward in cleanup job, but the consequences ripple. A wrong choice here turns into remove abandoned uploads under load, and the invariant you thought cleanup job guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Check DB linkage is the one most teams skip: drive cleanup job to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Delete orphans

Delete orphans is where most cleanup job implementations leak performance. A wrong choice here turns into remove abandoned uploads under load, and the invariant you thought cleanup job guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Delete orphans is the one most teams skip: drive cleanup job to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

test('deletes orphaned file', async () => {
  await cleanup('/tmp', 0);
  expect(fs.unlink).toHaveBeenCalled();
});

Pin the behavior with this test. A cleanup job setup that passes this test degrades on purpose instead of by accident, which is the difference between a managed failure and an incident.

Log the cleanup

The hard part of Log the cleanup is not the happy path — it is what happens when cleanup job fails halfway. A wrong choice here turns into remove abandoned uploads under load, and the invariant you thought cleanup job guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Log the cleanup is the one most teams skip: drive cleanup job to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Run on a schedule

Run on a schedule should be observable without a debugger. A wrong choice here turns into remove abandoned uploads under load, and the invariant you thought cleanup job guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Run on a schedule is the one most teams skip: drive cleanup job to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Test the job

Test the job is a trade, not a winner. A wrong choice here turns into remove abandoned uploads under load, and the invariant you thought cleanup job guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Test the job is the one most teams skip: drive cleanup job to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Common pitfalls

The common pitfalls in remove abandoned uploads all reduce to one mistake: treating cleanup job as a formality instead of a contract. The contract is the thing the rest of the system relies on, and a weak contract propagates weakness everywhere.

  • Skipping the cleanup job boundary so remove abandoned uploads enters unvalidated.
  • Assuming cleanup job is fast without measuring the tail latency under load.
  • Letting remove abandoned uploads fail silently instead of failing loudly with a metric.
  • Testing only the happy path so the cleanup job failure surfaces in production.
  • Sharing mutable state across requests so one bad input poisons the next.

Conclusion

Do not conflate the conclusion with the implementation. They look similar from the outside but the failure modes differ: cleanup job fails fast and the caller knows, while an unchecked path fails slow and the caller guesses. Choose the one whose failure mode your system can absorb, and document the choice at the call site.

Self-check: write a test that drives cleanup job to the failing condition and assert the system degrades gracefully. Then remove abandoned uploads is a property you maintain, not a feature you hope for.

Sources: Node fs, cron

FAQ

What max age?
24h for partials, 7d for unlinked completed uploads.
Dry run first?
Yes. Log what would be deleted before enabling the unlink.

Related Examples