This document describes the implementation of graceful Cloudinary upload failure handling to prevent clip loss when uploads fail.
Previously, if Cloudinary upload failed after FFmpeg successfully cut a clip, the clip would be lost because:
- The local temporary file was deleted regardless of upload success
- No retry mechanism existed for upload failures
- Failed uploads would cause the entire job to fail and retry from scratch (including re-cutting with FFmpeg)
The CloudinaryService.uploadVideoFromBuffer() method now includes built-in retry logic:
- 2 automatic retries (3 total attempts)
- Exponential backoff: 1000ms → 2000ms → 5000ms (capped)
- Retries are specific to the upload operation, not the entire job
// Example usage
const result = await cloudinaryService.uploadVideoFromBuffer(
buffer,
clipId,
{}, // options
2 // number of retries
);When all upload attempts fail:
- The clip is saved with
status: 'upload_failed' - The local file path is preserved in
clip.localFilePath - The local temporary file is NOT deleted
- An error message is stored in
clip.error
This allows for:
- Manual intervention and retry
- Serving clips from local storage as a temporary fallback
- Scheduled retry jobs to attempt upload again later
Added upload_failed status to the Clip entity:
status?: 'pending' | 'processing' | 'success' | 'failed' | 'upload_failed'failed: FFmpeg cutting failedupload_failed: FFmpeg succeeded, but Cloudinary upload failed
The ClipsService now includes a method to retry failed uploads:
const result = await clipsService.retryFailedUpload(clipId);This method:
- Validates the clip exists and has
upload_failedstatus - Checks that a local file path is available
- Re-enqueues the clip for upload (skips FFmpeg cutting)
-
clip.entity.ts
- Added
upload_failedstatus - Added
localFilePathfield for fallback storage
- Added
-
cloudinary.service.ts
- Refactored
uploadVideoFromBuffer()to include retry logic - Added
performUpload()private method for single upload attempt - Added
delay()helper for exponential backoff
- Refactored
-
clip-generation.processor.ts
- Modified upload error handling to preserve local file
- Returns clip with
upload_failedstatus instead of throwing - Only deletes local file after successful upload
-
clips.service.ts
- Added
retryFailedUpload()method for manual retry
- Added
-
clip-generation.processor.spec.ts
- Tests for upload failure handling
- Verifies local file preservation on failure
- Verifies local file deletion on success
-
cloudinary.service.spec.ts (new)
- Tests retry logic with various failure scenarios
- Verifies exponential backoff timing
- Tests success on different retry attempts
// Get all clips with failed uploads
const failedClips = clipsService.getClipsByStatus('upload_failed');
// Log details
failedClips.forEach(clip => {
console.log(`Clip ${clip.id} failed: ${clip.error}`);
console.log(`Local file: ${clip.localFilePath}`);
});// Retry a specific clip
const result = await clipsService.retryFailedUpload('clip-id-123');
if (result.success) {
console.log('Retry queued successfully');
} else {
console.error(`Retry failed: ${result.error}`);
}// Example cron job to retry failed uploads
@Cron('0 */6 * * *') // Every 6 hours
async retryFailedUploads() {
const failedClips = this.clipsService.getClipsByStatus('upload_failed');
for (const clip of failedClips) {
// Only retry clips that failed within last 24 hours
const hoursSinceFailure =
(Date.now() - clip.updatedAt.getTime()) / (1000 * 60 * 60);
if (hoursSinceFailure < 24) {
await this.clipsService.retryFailedUpload(clip.id);
}
}
}When migrating to Prisma, update the Clip model:
model Clip {
// ... existing fields
status String? // Add 'upload_failed' as valid value
error String? // Store error message
localFilePath String? // Store local file path for fallback
// ... rest of fields
}✅ Add 2 retries in processor
- Implemented in
CloudinaryService.uploadVideoFromBuffer()with configurable retry count
✅ On final fail: log, update clip status 'upload_failed'
- Logs error at ERROR level with full context
- Returns clip with
status: 'upload_failed' - Stores error message in
clip.error
✅ Keep local temp file as fallback
- Local file is NOT deleted when upload fails
- File path stored in
clip.localFilePath - Can be used for manual retry or temporary serving
- Scheduled Retry Jobs: Automatically retry failed uploads periodically
- Local File Serving: Serve clips from local storage when Cloudinary URL unavailable
- Upload Queue: Separate queue for upload retries with different priority
- Monitoring Dashboard: UI to view and manage failed uploads
- Cleanup Job: Remove old local files after successful retry or expiration