overleaf/overleaf · warning · NotReadyToDelete

refusing to delete non-expired project

Error message

refusing to delete non-expired project

What it means

deleteProjectBackup throws NotReadyToDelete when the project's deleterData.deletedAt plus EXPIRE_PROJECTS_AFTER_MS is in the future — the project is deleted but its grace/retention period has not elapsed. Deletion is intentionally deferred until the backup expires so users can still restore within the window.

Source

Thrown at services/history-v1/storage/lib/backupDeletion.mjs:51

 * @return {Promise<void>}
 */
async function deleteProjectBackup(projectId) {
  const deletedProject = await deletedProjectsCollection.findOne(
    { 'deleterData.deletedProjectId': new ObjectId(projectId) },
    {
      projection: {
        'deleterData.deletedProjectOverleafHistoryId': 1,
        'deleterData.deletedAt': 1,
      },
    }
  )
  if (!deletedProject) {
    throw new NotReadyToDelete('refusing to delete non-deleted project')
  }
  const expiresAt =
    deletedProject.deleterData.deletedAt.getTime() + EXPIRE_PROJECTS_AFTER_MS
  if (expiresAt > Date.now()) {
    throw new NotReadyToDelete('refusing to delete non-expired project')
  }

  const historyId =
    deletedProject.deleterData.deletedProjectOverleafHistoryId?.toString()
  if (!historyId) {
    throw new NotReadyToDelete(
      'refusing to delete project with unknown historyId'
    )
  }

  if (await projectHasLatestChunk(historyId)) {
    throw new NotReadyToDelete(
      'refusing to delete project with remaining chunks'
    )
  }

  const prefix = projectKey.format(historyId) + '/'
  await backupPersistor.deleteDirectory(chunksBucket, prefix)

View on GitHub (pinned to 28ad3b03b7)

Solutions

  1. Wait until the retention window (deletedAt + EXPIRE_PROJECTS_AFTER_MS) has passed and let the next scheduled run pick it up
  2. Skip the project gracefully — NotReadyToDelete here is expected during the retention window
  3. Verify EXPIRE_PROJECTS_AFTER_MS matches your intended retention policy; adjust only deliberately
  4. Check server clock synchronization (NTP) if projects seem expired but still throw

Example fix

// before
await deleteProjectBackup(projectId) // may throw NotReadyToDelete
// after
try {
  await deleteProjectBackup(projectId)
} catch (err) {
  if (err instanceof NotReadyToDelete) {
    console.log(`deferred: ${projectId}`)
    return
  }
  throw err
}
Defensive patterns

Strategy: try-catch

Validate before calling

const doc = await db.projects.findOne(
  { _id: projectId },
  { projection: { 'deleterData.deletedAt': 1 } }
)
if (doc && new Date(doc.deleterData.deletedAt.getTime() + EXPIRE_PROJECTS_AFTER_MS) > new Date()) {
  throw new Skip(`project ${projectId} still within retention window`)
}

Try / catch

try {
  await deleteProjectBackup(projectId)
} catch (err) {
  if (err instanceof NotReadyToDelete) {
    scheduleRetry(projectId, EXPIRE_PROJECTS_AFTER_MS)
    return
  }
  throw err
}

Prevention

When it happens

Trigger: deleteProjectBackup is called for a deleted project whose deletedAt timestamp is less than EXPIRE_PROJECTS_AFTER_MS ago: `expiresAt > Date.now()`.

Common situations: Cron or script running backup deletion too aggressively after deletion; EXPIRE_PROJECTS_AFTER_MS raised (retention policy change) so previously 'due' projects are no longer expired; clock skew between app servers and stored timestamps.

Related errors


AI-assisted analysis of overleaf/overleaf@28ad3b03b7 (2026-09-03). Data as JSON: /api/errors/fd6f206162aeb883. Report an issue: GitHub.