Loading content...
File processing is one of the most natural fits for serverless computing. Users upload files—images, videos, documents, archives—and those files need immediate processing: resizing, validation, extraction, transformation, or analysis. The event-driven nature of serverless perfectly matches this pattern: files arrive unpredictably, trigger processing, and results are stored.
Traditional file processing architectures required maintaining worker servers, managing queues, and handling capacity planning for peak loads. A media company might need 100 servers during a product launch but only 5 during normal operations. Serverless eliminates this waste: each file upload triggers exactly the compute needed, scaling from zero to thousands of concurrent processes.
This page provides a comprehensive guide to real-time file processing in serverless environments. We'll cover image manipulation, video processing, document handling, and strategies for files that exceed Lambda's constraints.
By the end of this page, you will understand: (1) How to build image processing pipelines with Lambda, (2) Video transcoding strategies using serverless and managed services, (3) Document processing patterns including PDF and Office files, (4) Strategies for handling files that exceed Lambda limits, (5) Performance optimization for file-heavy workloads, and (6) Security considerations for user-uploaded content.
Image processing is the canonical serverless file processing use case. Users upload images, and the system automatically generates thumbnails, optimizes for web delivery, extracts metadata, and applies content policies.
Common Image Processing Operations:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
import { S3Handler } from "aws-lambda";import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";import sharp from "sharp"; const s3 = new S3Client({}); interface ImageVariant { suffix: string; width: number; height?: number; format: "jpeg" | "webp" | "avif" | "png"; quality: number;} const VARIANTS: ImageVariant[] = [ { suffix: "thumb", width: 150, height: 150, format: "webp", quality: 80 }, { suffix: "small", width: 320, format: "webp", quality: 85 }, { suffix: "medium", width: 800, format: "webp", quality: 85 }, { suffix: "large", width: 1920, format: "webp", quality: 90 }, { suffix: "original", width: 4096, format: "webp", quality: 95 }]; export const handler: S3Handler = async (event) => { for (const record of event.Records) { const bucket = record.s3.bucket.name; const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " ")); // Skip if not in uploads folder or not an image if (!key.startsWith("uploads/") || !isImage(key)) { console.log(`Skipping: ${key}`); continue; } console.log(`Processing image: s3://${bucket}/${key}`); try { // Get the original image const original = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key })); const imageBuffer = Buffer.from(await original.Body!.transformToByteArray()); // Get image metadata const metadata = await sharp(imageBuffer).metadata(); console.log(`Original: ${metadata.width}x${metadata.height}, ${metadata.format}`); // Generate all variants const results = await Promise.all( VARIANTS.map(variant => generateVariant(imageBuffer, key, variant)) ); // Upload all variants await Promise.all( results.map(result => s3.send(new PutObjectCommand({ Bucket: process.env.OUTPUT_BUCKET!, Key: result.key, Body: result.buffer, ContentType: `image/${result.format}`, CacheControl: "public, max-age=31536000, immutable" })) ) ); console.log(`Generated ${results.length} variants for ${key}`); // Store metadata for API access await storeImageMetadata(key, metadata, results); } catch (error) { console.error(`Failed to process ${key}:`, error); throw error; } }}; async function generateVariant( original: Buffer, originalKey: string, variant: ImageVariant): Promise<{ key: string; buffer: Buffer; format: string }> { let pipeline = sharp(original); // Resize with smart cropping for thumbnails if (variant.height) { pipeline = pipeline.resize(variant.width, variant.height, { fit: "cover", position: "attention" // Smart crop focusing on interesting areas }); } else { pipeline = pipeline.resize(variant.width, undefined, { fit: "inside", withoutEnlargement: true }); } // Convert to target format switch (variant.format) { case "webp": pipeline = pipeline.webp({ quality: variant.quality }); break; case "avif": pipeline = pipeline.avif({ quality: variant.quality }); break; case "jpeg": pipeline = pipeline.jpeg({ quality: variant.quality, mozjpeg: true }); break; } const buffer = await pipeline.toBuffer(); const baseName = originalKey.replace("uploads/", "").replace(/\.[^.]+$/, ""); return { key: `processed/${baseName}-${variant.suffix}.${variant.format}`, buffer, format: variant.format };} function isImage(key: string): boolean { const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".bmp"]; return imageExtensions.some(ext => key.toLowerCase().endsWith(ext));}Layer Configuration for Sharp:
The Sharp library requires native binaries. For Lambda, you need a layer with Linux-compatible binaries:
# Build sharp layer for Lambda
npm install --platform=linux --arch=x64 sharp
# Or use pre-built layer from community
Alternatively, use Lambda's container image support to include Sharp in a Docker image.
| Format | Best For | Browser Support | Compression |
|---|---|---|---|
| WebP | General web use | 95%+ browsers | 25-35% smaller than JPEG |
| AVIF | Maximum compression | ~90% browsers | 50% smaller than JPEG |
| JPEG | Photos, fallback | Universal | Baseline standard |
| PNG | Transparency, graphics | Universal | Lossless, larger files |
| SVG | Icons, illustrations | Universal | Infinitely scalable |
For high-volume image serving, consider on-demand processing with Lambda@Edge or CloudFront Functions. Instead of pre-generating all variants, generate them on first request and cache at the CDN. This reduces storage costs and processes only the variants actually requested.
Video processing presents unique challenges for serverless: files are large, processing is CPU-intensive, and transcoding can take hours for long videos. Multiple strategies exist to handle video in serverless architectures.
AWS MediaConvert (Recommended for Most Cases):
MediaConvert is a fully managed video transcoding service:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
import { S3Handler } from "aws-lambda";import { MediaConvertClient, CreateJobCommand, CreateJobRequest } from "@aws-sdk/client-mediaconvert"; const mediaConvert = new MediaConvertClient({ endpoint: process.env.MEDIACONVERT_ENDPOINT}); export const handler: S3Handler = async (event) => { for (const record of event.Records) { const bucket = record.s3.bucket.name; const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " ")); if (!isVideo(key)) continue; const inputUri = `s3://${bucket}/${key}`; const outputPrefix = key.replace("uploads/", "transcoded/").replace(/\.[^.]+$/, ""); const jobSettings: CreateJobRequest = { Role: process.env.MEDIACONVERT_ROLE_ARN!, Settings: { Inputs: [{ FileInput: inputUri, AudioSelectors: { "Audio Selector 1": { DefaultSelection: "DEFAULT" } }, VideoSelector: {} }], OutputGroups: [ // HLS for adaptive streaming { Name: "HLS Group", OutputGroupSettings: { Type: "HLS_GROUP_SETTINGS", HlsGroupSettings: { Destination: `s3://${process.env.OUTPUT_BUCKET}/${outputPrefix}/hls/`, SegmentLength: 6, MinSegmentLength: 2 } }, Outputs: [ // 1080p { VideoDescription: { Width: 1920, Height: 1080, CodecSettings: { Codec: "H_264", H264Settings: { RateControlMode: "QVBR", MaxBitrate: 8000000, QvbrSettings: { QvbrQualityLevel: 8 } } } }, AudioDescriptions: [{ CodecSettings: { Codec: "AAC", AacSettings: { Bitrate: 192000, SampleRate: 48000 } } }], ContainerSettings: { Container: "M3U8" } }, // 720p { VideoDescription: { Width: 1280, Height: 720, CodecSettings: { Codec: "H_264", H264Settings: { RateControlMode: "QVBR", MaxBitrate: 5000000, QvbrSettings: { QvbrQualityLevel: 7 } } } }, AudioDescriptions: [{ CodecSettings: { Codec: "AAC", AacSettings: { Bitrate: 128000, SampleRate: 48000 } } }], ContainerSettings: { Container: "M3U8" } }, // 480p { VideoDescription: { Width: 854, Height: 480, CodecSettings: { Codec: "H_264", H264Settings: { RateControlMode: "QVBR", MaxBitrate: 2500000, QvbrSettings: { QvbrQualityLevel: 6 } } } }, AudioDescriptions: [{ CodecSettings: { Codec: "AAC", AacSettings: { Bitrate: 96000, SampleRate: 48000 } } }], ContainerSettings: { Container: "M3U8" } } ] }, // MP4 for download { Name: "MP4 Group", OutputGroupSettings: { Type: "FILE_GROUP_SETTINGS", FileGroupSettings: { Destination: `s3://${process.env.OUTPUT_BUCKET}/${outputPrefix}/mp4/` } }, Outputs: [{ VideoDescription: { Width: 1920, Height: 1080, CodecSettings: { Codec: "H_264", H264Settings: { RateControlMode: "QVBR", MaxBitrate: 10000000, QvbrSettings: { QvbrQualityLevel: 9 } } } }, AudioDescriptions: [{ CodecSettings: { Codec: "AAC", AacSettings: { Bitrate: 256000, SampleRate: 48000 } } }], ContainerSettings: { Container: "MP4", Mp4Settings: {} } }] } ] } }; const result = await mediaConvert.send(new CreateJobCommand(jobSettings)); console.log(`Created MediaConvert job: ${result.Job?.Id}`); }}; function isVideo(key: string): boolean { const videoExtensions = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"]; return videoExtensions.some(ext => key.toLowerCase().endsWith(ext));}Thumbnail Generation from Video:
Media Convert can also extract thumbnails at specified intervals or using smart frame selection. For simple thumbnail extraction, Lambda with FFmpeg can work for short videos:
Video processing costs can add up quickly. MediaConvert charges per minute of output. For a 10-minute video with 3 quality variants, you pay for 30 minutes of transcoding. Consider offering fewer variants for lower-tier users, or transcode on-demand for rarely-accessed content.
Document processing encompasses PDF manipulation, Office file conversion, text extraction, and content analysis. Serverless handles these workloads effectively, with various approaches depending on the document type.
PDF Processing:
PDF operations include:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
import { S3Handler } from "aws-lambda";import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";import { PDFDocument, StandardFonts, rgb } from "pdf-lib"; const s3 = new S3Client({}); /** * Add watermark to uploaded PDFs */export const handler: S3Handler = async (event) => { for (const record of event.Records) { const bucket = record.s3.bucket.name; const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " ")); if (!key.endsWith(".pdf")) continue; try { // Get original PDF const response = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key })); const pdfBytes = await response.Body!.transformToByteArray(); // Load PDF const pdfDoc = await PDFDocument.load(pdfBytes); const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica); // Add watermark to each page const pages = pdfDoc.getPages(); for (const page of pages) { const { width, height } = page.getSize(); page.drawText("CONFIDENTIAL", { x: width / 2 - 100, y: height / 2, size: 50, font: helvetica, color: rgb(0.75, 0.75, 0.75), opacity: 0.3, rotate: { angle: 45, type: "degrees" } }); } // Add metadata pdfDoc.setTitle("Processed Document"); pdfDoc.setModificationDate(new Date()); pdfDoc.setProducer("Document Processing System"); // Save and upload const modifiedPdf = await pdfDoc.save(); const outputKey = key.replace("uploads/", "watermarked/"); await s3.send(new PutObjectCommand({ Bucket: process.env.OUTPUT_BUCKET!, Key: outputKey, Body: modifiedPdf, ContentType: "application/pdf" })); console.log(`Watermarked: ${key} -> ${outputKey}`); } catch (error) { console.error(`Failed to process ${key}:`, error); throw error; } }};Text Extraction with Amazon Textract:
For extracting text from documents, including scanned images and complex layouts, Amazon Textract provides AI-powered extraction:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
import { S3Handler } from "aws-lambda";import { TextractClient, DetectDocumentTextCommand, AnalyzeDocumentCommand } from "@aws-sdk/client-textract";import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; const textract = new TextractClient({});const s3 = new S3Client({}); interface ExtractedDocument { fileName: string; extractedAt: string; pageCount: number; text: string; tables: any[]; forms: Record<string, string>;} export const handler: S3Handler = async (event) => { for (const record of event.Records) { const bucket = record.s3.bucket.name; const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " ")); try { // Analyze document with Textract const analysis = await textract.send(new AnalyzeDocumentCommand({ Document: { S3Object: { Bucket: bucket, Name: key } }, FeatureTypes: ["TABLES", "FORMS"] })); // Extract text blocks const textBlocks = analysis.Blocks?.filter(b => b.BlockType === "LINE") || []; const text = textBlocks.map(b => b.Text).join("\n"); // Extract tables const tables = extractTables(analysis.Blocks || []); // Extract form key-value pairs const forms = extractForms(analysis.Blocks || []); const extracted: ExtractedDocument = { fileName: key, extractedAt: new Date().toISOString(), pageCount: analysis.Blocks?.filter(b => b.BlockType === "PAGE").length || 1, text, tables, forms }; // Store extracted content const outputKey = key.replace(/\.[^.]+$/, ".json"); await s3.send(new PutObjectCommand({ Bucket: process.env.OUTPUT_BUCKET!, Key: `extracted/${outputKey}`, Body: JSON.stringify(extracted, null, 2), ContentType: "application/json" })); console.log(`Extracted ${text.length} characters from ${key}`); } catch (error) { console.error(`Textract failed for ${key}:`, error); throw error; } }}; function extractForms(blocks: any[]): Record<string, string> { const forms: Record<string, string> = {}; const keyMap = new Map<string, string>(); const valueMap = new Map<string, string>(); for (const block of blocks) { if (block.BlockType === "KEY_VALUE_SET") { const text = getBlockText(block, blocks); if (block.EntityTypes?.includes("KEY")) { keyMap.set(block.Id, text); } else { valueMap.set(block.Id, text); } } } // Match keys to values for (const block of blocks) { if (block.BlockType === "KEY_VALUE_SET" && block.EntityTypes?.includes("KEY")) { const keyText = keyMap.get(block.Id) || ""; const valueId = block.Relationships?.find((r: any) => r.Type === "VALUE")?.Ids?.[0]; const valueText = valueId ? valueMap.get(valueId) || "" : ""; if (keyText) { forms[keyText] = valueText; } } } return forms;}| Service/Library | Use Case | Serverless Compatible | Cost Model |
|---|---|---|---|
| pdf-lib | PDF manipulation (merge, split, watermark) | Yes (pure JS) | Free, open source |
| Amazon Textract | AI text/table/form extraction | Yes | Per page analyzed |
| Amazon Comprehend | NLP (sentiment, entities) | Yes | Per 100 characters |
| LibreOffice (container) | Office → PDF conversion | Yes (via container) | Compute cost only |
| Pandoc (container) | Format conversion | Yes (via container) | Compute cost only |
Tools like LibreOffice or Pandoc require large binaries not suitable for Lambda layers. Use Lambda container images (up to 10GB) to package these tools. Build images with only necessary components to keep cold starts reasonable.
Lambda has memory limits (up to 10GB) and ephemeral storage limits (up to 10GB with configuration). For files larger than these limits, or processing that exceeds 15 minutes, alternative strategies are needed.
Strategy 1: Streaming Processing
Process data in streams without loading entire file into memory:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
import { S3Handler } from "aws-lambda";import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";import { Readable, Transform, pipeline } from "stream";import { promisify } from "util";import { createGzip } from "zlib"; const s3 = new S3Client({});const pipelineAsync = promisify(pipeline); /** * Process large CSV files line-by-line without loading into memory */export const handler: S3Handler = async (event) => { for (const record of event.Records) { const bucket = record.s3.bucket.name; const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " ")); // Get file as stream const response = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key })); const inputStream = response.Body as Readable; // Create transform stream let lineCount = 0; const transformer = new Transform({ transform(chunk, encoding, callback) { const lines = chunk.toString().split("\n"); lines.forEach((line: string) => { if (line.trim()) { lineCount++; // Transform each line (example: add line number) this.push(`${lineCount}:${line}\n`); } }); callback(); } }); // Collect output (for small results) or stream to S3 const chunks: Buffer[] = []; transformer.on("data", chunk => chunks.push(chunk)); await pipelineAsync(inputStream, transformer); const output = Buffer.concat(chunks); // Upload result await s3.send(new PutObjectCommand({ Bucket: process.env.OUTPUT_BUCKET!, Key: key.replace("uploads/", "processed/"), Body: output })); console.log(`Processed ${lineCount} lines`); }};Strategy 2: Chunked Processing with S3 Multipart
For operations that can be parallelized, split the file into chunks:
Strategy 3: AWS Fargate for Very Large Files
For files requiring hours of processing or exceeding Lambda limits:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
import { S3Handler } from "aws-lambda";import { ECSClient, RunTaskCommand } from "@aws-sdk/client-ecs";import { S3Client, HeadObjectCommand } from "@aws-sdk/client-s3"; const ecs = new ECSClient({});const s3 = new S3Client({}); const SIZE_THRESHOLD = 500 * 1024 * 1024; // 500MB export const handler: S3Handler = async (event) => { for (const record of event.Records) { const bucket = record.s3.bucket.name; const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " ")); // Check file size const metadata = await s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key })); const fileSize = metadata.ContentLength || 0; if (fileSize > SIZE_THRESHOLD) { console.log(`Large file (${(fileSize / 1024 / 1024).toFixed(1)}MB), dispatching to Fargate`); // Run Fargate task for heavy processing await ecs.send(new RunTaskCommand({ cluster: process.env.ECS_CLUSTER!, taskDefinition: process.env.TASK_DEFINITION!, launchType: "FARGATE", networkConfiguration: { awsvpcConfiguration: { subnets: process.env.SUBNETS!.split(","), assignPublicIp: "ENABLED" } }, overrides: { containerOverrides: [{ name: "file-processor", environment: [ { name: "INPUT_BUCKET", value: bucket }, { name: "INPUT_KEY", value: key }, { name: "OUTPUT_BUCKET", value: process.env.OUTPUT_BUCKET! } ] }] } })); } else { // Process in Lambda await processFileInLambda(bucket, key); } }};| File Size | Strategy | Max Duration | Memory Needed |
|---|---|---|---|
| < 100MB | Lambda, load to memory | 15 min | 2x file size |
| 100MB - 500MB | Lambda, streaming | 15 min | 256MB-1GB |
| 500MB - 5GB | Lambda, ephemeral storage | 15 min | 10GB storage |
| 5GB+ | Fargate | Hours/days | Up to 120GB |
| Parallelizable | Step Functions Map | Unlimited | Variable |
Lambda now supports up to 10GB of ephemeral storage (/tmp). Configure via EphemeralStorage setting in your function configuration. This enables processing of larger files without streaming, though you pay for the additional storage.
User-uploaded content poses security risks. Files may contain malware, inappropriate content, or attempt to exploit processing vulnerabilities. A robust file processing pipeline includes multiple security layers.
Malware Scanning:
Scan files before processing or serving:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
import { S3Handler } from "aws-lambda";import { S3Client, GetObjectCommand, DeleteObjectCommand, CopyObjectCommand } from "@aws-sdk/client-s3";import { execSync } from "child_process";import { writeFileSync, unlinkSync, mkdirSync } from "fs";import { join } from "path"; const s3 = new S3Client({});const TMP_DIR = "/tmp/scan"; // Ensure ClamAV definitions are updated (via layer or container)const CLAMSCAN_PATH = "/opt/bin/clamscan";const VIRUS_DEFINITIONS = "/opt/share/clamav"; export const handler: S3Handler = async (event) => { mkdirSync(TMP_DIR, { recursive: true }); for (const record of event.Records) { const bucket = record.s3.bucket.name; const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " ")); const localPath = join(TMP_DIR, "file-to-scan"); try { // Download file const response = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key })); const buffer = Buffer.from(await response.Body!.transformToByteArray()); writeFileSync(localPath, buffer); // Scan with ClamAV try { execSync(`${CLAMSCAN_PATH} --database=${VIRUS_DEFINITIONS} ${localPath}`, { timeout: 60000 // 60 second timeout }); // Clean - move to approved bucket console.log(`${key}: CLEAN`); await s3.send(new CopyObjectCommand({ Bucket: process.env.APPROVED_BUCKET!, Key: key, CopySource: `${bucket}/${key}` })); } catch (scanError: any) { if (scanError.status === 1) { // Virus found console.error(`${key}: INFECTED - ${scanError.stdout}`); // Delete infected file await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key })); // Alert security team await notifySecurityTeam(key, scanError.stdout); } else { throw scanError; // Scan error, not infection } } } finally { // Clean up try { unlinkSync(localPath); } catch {} } }}; async function notifySecurityTeam(file: string, details: string) { // Send to SNS, Slack, PagerDuty, etc. console.error(`SECURITY ALERT: Infected file uploaded: ${file}`);}Content Moderation:
For user-generated images and videos, automated moderation identifies inappropriate content:
Amazon Rekognition:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
import { RekognitionClient, DetectModerationLabelsCommand } from "@aws-sdk/client-rekognition"; const rekognition = new RekognitionClient({}); interface ModerationResult { isSafe: boolean; confidence: number; flags: string[]; details: Array<{ label: string; confidence: number }>;} export async function moderateImage( bucket: string, key: string): Promise<ModerationResult> { const response = await rekognition.send(new DetectModerationLabelsCommand({ Image: { S3Object: { Bucket: bucket, Name: key } }, MinConfidence: 75 })); const labels = response.ModerationLabels || []; // Define blocked categories const blockedCategories = [ "Explicit Nudity", "Violence", "Visually Disturbing", "Drugs", "Hate Symbols" ]; const flags = labels .filter(label => blockedCategories.some(blocked => label.ParentName === blocked || label.Name === blocked ) ) .map(label => label.Name!); const maxConfidence = labels.length > 0 ? Math.max(...labels.map(l => l.Confidence || 0)) : 0; return { isSafe: flags.length === 0, confidence: 100 - maxConfidence, flags, details: labels.map(l => ({ label: l.Name!, confidence: l.Confidence || 0 })) };}Treat every uploaded file as potentially malicious. Validate everything: file type, size, content, metadata. Process uploads in isolated environments. Never execute or include user uploads in your application without thorough validation.
File processing workloads benefit from specific optimizations that differ from typical API workloads.
Memory and CPU Optimization:
Lambda CPU scales linearly with memory. For CPU-intensive operations like image processing or compression, increasing memory can significantly reduce execution time and cost:
| Memory | vCPU Equivalent | Image Resize Duration | Cost per Execution |
|---|---|---|---|
| 512MB | ~0.3 vCPU | 2,500ms | $0.000021 |
| 1024MB | ~0.6 vCPU | 1,300ms | $0.000022 |
| 1769MB | 1 vCPU | 850ms | $0.000024 |
| 3008MB | ~1.7 vCPU | 550ms | $0.000027 |
| 10240MB | 6 vCPU | 180ms | $0.000031 |
Parallel Processing:
When processing multiple outputs (e.g., multiple image sizes), run operations in parallel:
123456789101112131415161718192021222324252627282930313233
// Sequential: Total time = sum of all operationsasync function processSequential(image: Buffer, variants: Variant[]) { const results = []; for (const variant of variants) { results.push(await generateVariant(image, variant)); } return results;}// If each variant takes 500ms, 5 variants = 2,500ms // Parallel: Total time = longest operationasync function processParallel(image: Buffer, variants: Variant[]) { return Promise.all( variants.map(variant => generateVariant(image, variant)) );}// If each variant takes 500ms, 5 variants = ~500ms (if CPU allows) // Controlled parallelism: Balance CPU and memoryasync function processControlled(image: Buffer, variants: Variant[], concurrency: number) { const results: any[] = []; for (let i = 0; i < variants.length; i += concurrency) { const batch = variants.slice(i, i + concurrency); const batchResults = await Promise.all( batch.map(variant => generateVariant(image, variant)) ); results.push(...batchResults); } return results;}// Concurrency of 2: 5 variants = ~1,500ms, lower peak memoryCaching and Preprocessing:
Use the open-source AWS Lambda Power Tuning tool to automatically find the optimal memory configuration. It runs your function at different memory settings and graphs performance vs. cost, helping you find the sweet spot for your specific workload.
Well-designed file processing systems follow architectural patterns that handle failures gracefully, provide visibility, and scale effectively.
Pattern 1: Quarantine-Process-Approve
New uploads go to a quarantine bucket, are processed and validated, then moved to an approved bucket if they pass:
Pattern 2: Processing Pipeline with Status Tracking
For complex processing with multiple stages, track status in a database:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
interface FileProcessingRecord { fileId: string; originalKey: string; uploadedBy: string; uploadedAt: string; status: "uploaded" | "scanning" | "moderating" | "processing" | "complete" | "failed"; stages: { malwareScan?: { status: string; completedAt?: string }; moderation?: { status: string; flags?: string[]; completedAt?: string }; processing?: { status: string; outputs?: string[]; completedAt?: string }; }; error?: { stage: string; message: string; timestamp: string }; completedAt?: string;} // Update status at each stageasync function updateProcessingStatus( fileId: string, stage: string, update: Record<string, any>): Promise<void> { await dynamodb.send(new UpdateItemCommand({ TableName: "FileProcessing", Key: { fileId: { S: fileId } }, UpdateExpression: `SET stages.#stage = :update, #status = :status, updatedAt = :now`, ExpressionAttributeNames: { "#stage": stage, "#status": "status" }, ExpressionAttributeValues: { ":update": { M: marshall(update) }, ":status": { S: stage }, ":now": { S: new Date().toISOString() } } }));} // API endpoint to check processing statusexport const statusHandler: APIGatewayProxyHandlerV2 = async (event) => { const fileId = event.pathParameters?.fileId; const result = await dynamodb.send(new GetItemCommand({ TableName: "FileProcessing", Key: { fileId: { S: fileId! } } })); if (!result.Item) { return { statusCode: 404, body: JSON.stringify({ error: "File not found" }) }; } return { statusCode: 200, body: JSON.stringify(unmarshall(result.Item)) };};Pattern 3: Step Functions Orchestration
For complex workflows with branching, retries, and human-in-the-loop approval, Step Functions provide visual orchestration:
| Pattern | Complexity | Best For | Visibility |
|---|---|---|---|
| Direct S3 → Lambda | Low | Simple transformations | CloudWatch logs only |
| SQS Buffered | Medium | Bursty uploads, retry needed | Queue metrics, DLQ |
| Status Tracking (DB) | Medium | User-facing status | API queryable |
| Step Functions | High | Multi-stage, approvals | Visual workflow |
For user-uploaded content, provide immediate feedback. Return success after upload (not after processing), then process asynchronously. Provide a status endpoint or use WebSockets to notify when processing completes. Users shouldn't wait for 30-second image processing before seeing confirmation.
Real-time file processing showcases serverless at its best: unpredictable workloads trigger exactly the compute needed, scale automatically, and cost nothing when idle. From image thumbnails to video transcoding to document extraction, these patterns enable sophisticated file processing without managing infrastructure.
Let's consolidate the key takeaways:
Module Complete:
With this page, you've completed the Serverless Patterns module. You've learned five powerful patterns for serverless computing: event-driven processing, API backends, scheduled tasks, data processing pipelines, and real-time file processing. These patterns form the foundation of most serverless architectures, enabling you to build scalable, cost-effective systems for a wide variety of use cases.
Congratulations! You now have comprehensive knowledge of serverless patterns for file processing. Combined with the other patterns in this module—event-driven processing, APIs, scheduling, and data pipelines—you're equipped to architect serverless solutions for almost any use case. The next module will explore the limitations and trade-offs of serverless computing, helping you understand when serverless is (and isn't) the right choice.