Skip to content

TTL & Cleanup ​

Torrin includes built-in mechanisms to automatically expire and clean up abandoned uploads. This prevents storage bloat from incomplete or interrupted uploads.

What is TTL? ​

TTL (Time To Live) is the maximum lifetime of an upload session. After the TTL expires, the upload is considered abandoned and eligible for cleanup.

Default TTL: 24 hours

How TTL Works ​

  1. Upload Created: TTL timer starts when upload is initialized
  2. Updates Reset: Each chunk upload updates the updatedAt timestamp (but not the expiration)
  3. Expiration: After TTL time passes, upload status becomes "expired"
  4. Cleanup: Expired uploads can be removed with cleanup methods
typescript
const service = new TorrinService({
  storage,
  store,
  uploadTtlMs: 24 * 60 * 60 * 1000, // 24 hours (default)
});

Configuring TTL ​

Server-Side Configuration ​

typescript
import { createTorrinExpressRouter } from '@torrin-kit/server-express';

const router = createTorrinExpressRouter({
  storage,
  store,
  uploadTtlMs: 48 * 60 * 60 * 1000, // 48 hours
});
typescript
import { TorrinModule } from '@torrin-kit/server-nestjs';

@Module({
  imports: [
    TorrinModule.forRoot({
      storage,
      store,
      uploadTtlMs: 48 * 60 * 60 * 1000, // 48 hours
    }),
  ],
})
export class AppModule {}
Use CaseRecommended TTLReasoning
Small files (<10MB)1-6 hoursQuick uploads, less likely to need resume
Medium files (10MB-1GB)12-24 hoursBalance between storage and user convenience
Large files (>1GB)24-72 hoursLonger uploads, more interruptions possible
Development/Testing1-2 hoursFaster cleanup for rapid iteration

Manual Cleanup Methods ​

Clean Expired Uploads ​

Removes all uploads past their TTL:

typescript
import { TorrinService } from '@torrin-kit/server';

const service = new TorrinService({ storage, store, uploadTtlMs });

const result = await service.cleanupExpiredUploads();
console.log(`Cleaned ${result.cleaned} expired uploads`);
console.log(`Errors:`, result.errors);

Returns:

typescript
interface CleanupResult {
  cleaned: number;      // Number of successfully cleaned uploads
  errors: string[];     // Error messages for failed cleanups
}

Clean Stale Uploads ​

Removes uploads not updated within a specific time (regardless of TTL):

typescript
// Remove uploads older than 12 hours
const maxAgeMs = 12 * 60 * 60 * 1000;
const result = await service.cleanupStaleUploads(maxAgeMs);

This is useful for:

  • Cleaning up truly abandoned uploads faster than TTL
  • Emergency cleanup when storage is running low
  • Different cleanup policies for different file sizes

Automatic Cleanup ​

Run cleanup automatically at regular intervals:

typescript
// Clean up every hour
setInterval(async () => {
  try {
    const result = await service.cleanupExpiredUploads();
    if (result.cleaned > 0) {
      console.log(`[Cleanup] Removed ${result.cleaned} expired uploads`);
    }
    if (result.errors.length > 0) {
      console.error(`[Cleanup] Errors:`, result.errors);
    }
  } catch (error) {
    console.error('[Cleanup] Failed:', error);
  }
}, 60 * 60 * 1000);

Cron Job ​

Use a cron job for production environments:

Node.js with node-cron:

typescript
import cron from 'node-cron';
import { TorrinService } from '@torrin-kit/server';

const service = new TorrinService({ storage, store });

// Run cleanup every day at 3 AM
cron.schedule('0 3 * * *', async () => {
  console.log('[Cron] Starting upload cleanup...');
  const result = await service.cleanupExpiredUploads();
  console.log(`[Cron] Cleaned ${result.cleaned} uploads`);
});

Unix Cron:

bash
# /etc/crontab or crontab -e
# Run cleanup script every day at 3 AM
0 3 * * * node /path/to/cleanup-script.js

cleanup-script.js:

javascript
const { TorrinService } = require('@torrin-kit/server');
const { createLocalStorageDriver } = require('@torrin-kit/storage-local');
const { createInMemoryStore } = require('@torrin-kit/server');

const service = new TorrinService({
  storage: createLocalStorageDriver({ baseDir: './uploads' }),
  store: createInMemoryStore(),
});

(async () => {
  const result = await service.cleanupExpiredUploads();
  console.log(`Cleaned ${result.cleaned} uploads`);
  process.exit(0);
})();

Cleanup Endpoint ​

Create an admin endpoint for manual cleanup:

typescript
import express from 'express';

const app = express();

app.post('/admin/cleanup', authenticateAdmin, async (req, res) => {
  try {
    const result = await service.cleanupExpiredUploads();
    res.json({
      success: true,
      cleaned: result.cleaned,
      errors: result.errors,
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message,
    });
  }
});
typescript
import { Controller, Post, UseGuards } from '@nestjs/common';
import { InjectTorrin, TorrinService } from '@torrin-kit/server-nestjs';
import { AdminGuard } from './guards/admin.guard';

@Controller('admin')
export class AdminController {
  constructor(
    @InjectTorrin() private readonly torrin: TorrinService
  ) {}

  @Post('cleanup')
  @UseGuards(AdminGuard)
  async cleanup() {
    const result = await this.torrin.cleanupExpiredUploads();
    return {
      success: true,
      cleaned: result.cleaned,
      errors: result.errors,
    };
  }
}

Cleanup Behavior ​

What Gets Cleaned ​

  1. Upload Session: Metadata and state removed from store
  2. Temporary Chunks: All chunk files deleted from storage
  3. Final File: If completed but expired, removed from storage (configurable)

What Doesn't Get Cleaned ​

  • Completed uploads within TTL
  • Active uploads (recently updated)
  • Uploads in progress (currently uploading chunks)

Cleanup Process ​

mermaid
graph TD
    A[Start Cleanup] --> B[Query Expired Sessions]
    B --> C{Any Expired?}
    C -->|No| D[Return: cleaned=0]
    C -->|Yes| E[For Each Session]
    E --> F[Delete Temp Chunks]
    F --> G[Delete Session Data]
    G --> H{More Sessions?}
    H -->|Yes| E
    H -->|No| I[Return Results]

Storage-Specific Cleanup ​

Local Filesystem ​

typescript
import { createLocalStorageDriver } from '@torrin-kit/storage-local';

const storage = createLocalStorageDriver({
  baseDir: './uploads',
  tempDir: './uploads/.temp',
});

// Cleanup removes:
// - ./uploads/.temp/{uploadId}/* (all chunks)
// - Session data from store

S3 Storage ​

typescript
import { createS3StorageDriver } from '@torrin-kit/storage-s3';

const storage = createS3StorageDriver({
  bucket: 'my-uploads',
  region: 'us-east-1',
  keyPrefix: 'temp/',
});

// Cleanup removes:
// - s3://my-uploads/temp/{uploadId}/* (all chunks)
// - Session data from store

Note: S3 cleanup can incur API costs. Consider:

  • Using S3 Lifecycle Policies to auto-delete temp objects
  • Running cleanup less frequently
  • Batching cleanup operations

Store Persistence ​

In-Memory Store ​

typescript
import { createInMemoryStore } from '@torrin-kit/server';

const store = createInMemoryStore();

Limitations:

  • State lost on restart
  • No cleanup needed on restart (all sessions gone)
  • TTL checked in-memory

Persistent Store (Redis/Database) ​

For production, use a persistent store:

typescript
// Custom Redis store with TTL support
class RedisStore implements TorrinUploadStore {
  async createSession(init, chunkSize, ttlMs) {
    const session = { ...init, uploadId: generateId() };
    
    // Redis native TTL
    await redis.set(
      `session:${session.uploadId}`,
      JSON.stringify(session),
      'PX', // milliseconds
      ttlMs
    );
    
    return session;
  }
  
  // ... other methods
}

Benefits:

  • Sessions persist across restarts
  • Automatic expiration with Redis TTL
  • Scalable across multiple servers

Monitoring Cleanup ​

Logging Cleanup Activity ​

typescript
async function cleanupWithLogging() {
  const startTime = Date.now();
  const result = await service.cleanupExpiredUploads();
  const duration = Date.now() - startTime;
  
  // Log to monitoring service
  logger.info('Upload cleanup completed', {
    cleaned: result.cleaned,
    errors: result.errors.length,
    duration,
    timestamp: new Date().toISOString(),
  });
  
  // Alert if too many errors
  if (result.errors.length > 10) {
    alerting.send('High cleanup error rate', result.errors);
  }
}

Metrics ​

Track cleanup metrics for monitoring:

typescript
import { Counter, Histogram } from 'prom-client';

const cleanupCounter = new Counter({
  name: 'torrin_cleanup_total',
  help: 'Total cleanups performed',
  labelNames: ['status'],
});

const cleanupDuration = new Histogram({
  name: 'torrin_cleanup_duration_seconds',
  help: 'Cleanup duration in seconds',
});

async function cleanupWithMetrics() {
  const end = cleanupDuration.startTimer();
  
  try {
    const result = await service.cleanupExpiredUploads();
    cleanupCounter.inc({ status: 'success' }, result.cleaned);
    return result;
  } catch (error) {
    cleanupCounter.inc({ status: 'error' });
    throw error;
  } finally {
    end();
  }
}

Best Practices ​

  1. Set Appropriate TTL

    • Longer TTL = better UX (more time to resume)
    • Shorter TTL = less storage waste
    • Balance based on typical file sizes and upload times
  2. Regular Cleanup Schedule

    • Run cleanup at least daily
    • Off-peak hours (3-4 AM) for heavy cleanup
    • More frequent for high-volume systems
  3. Monitor Storage Usage

    • Track temp storage size
    • Alert on unusual growth
    • Adjust TTL if needed
  4. Error Handling

    • Log cleanup errors
    • Retry failed cleanups
    • Alert on repeated failures
  5. Gradual Rollout

    • Start with longer TTL (72h)
    • Monitor abandonment rates
    • Adjust based on user behavior
  6. Testing Cleanup

    • Test cleanup in staging first
    • Verify temp files are removed
    • Check for storage leaks

Troubleshooting ​

Cleanup Not Running ​

Problem: Expired uploads not being removed

Solutions:

  • Verify cleanup job is scheduled and running
  • Check store supports listExpiredSessions()
  • Ensure TTL is configured properly
  • Check for errors in cleanup logs

High Storage Usage ​

Problem: Temp storage growing despite cleanup

Solutions:

  • Reduce TTL
  • Run cleanup more frequently
  • Check for failed cleanups (orphaned files)
  • Verify cleanup process has permissions

Cleanup Takes Too Long ​

Problem: Cleanup blocking other operations

Solutions:

  • Batch cleanup operations
  • Run cleanup during off-peak hours
  • Use pagination for large cleanup jobs
  • Consider background job queue

Examples ​

Complete Production Setup ​

typescript
import express from 'express';
import cron from 'node-cron';
import { TorrinService } from '@torrin-kit/server';
import { createTorrinExpressRouter } from '@torrin-kit/server-express';
import { createS3StorageDriver } from '@torrin-kit/storage-s3';
import { createRedisStore } from './stores/redis';

const service = new TorrinService({
  storage: createS3StorageDriver({
    bucket: process.env.S3_BUCKET,
    region: process.env.AWS_REGION,
  }),
  store: createRedisStore(process.env.REDIS_URL),
  uploadTtlMs: 24 * 60 * 60 * 1000, // 24 hours
});

const app = express();

// Mount router
app.use('/api/uploads', createTorrinExpressRouter({
  storage: service.storage,
  store: service.store,
  uploadTtlMs: service.uploadTtlMs,
}));

// Cleanup every day at 3 AM
cron.schedule('0 3 * * *', async () => {
  console.log('[Cleanup] Starting...');
  const result = await service.cleanupExpiredUploads();
  console.log(`[Cleanup] Removed ${result.cleaned} uploads`);
  if (result.errors.length > 0) {
    console.error('[Cleanup] Errors:', result.errors);
  }
});

// Admin endpoint for manual cleanup
app.post('/admin/cleanup', authenticateAdmin, async (req, res) => {
  const result = await service.cleanupExpiredUploads();
  res.json(result);
});

app.listen(3000);

Next Steps ​