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
- Wait until the retention window (deletedAt + EXPIRE_PROJECTS_AFTER_MS) has passed and let the next scheduled run pick it up
- Skip the project gracefully — NotReadyToDelete here is expected during the retention window
- Verify EXPIRE_PROJECTS_AFTER_MS matches your intended retention policy; adjust only deliberately
- 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
- Schedule deletion jobs no earlier than deletedAt + retention period
- Keep EXPIRE_PROJECTS_AFTER_MS consistent between scheduler and deleter
- Sync server clocks (NTP) to avoid premature/late expiry
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.