Loading learning content...
In the previous pages, we've established that tiered storage can reduce costs by 60-90%. But these savings come at a price: retrieval latency. Hot storage delivers data in milliseconds. Cold archive storage might take hours—or even days.
This creates one of the most critical trade-offs in storage architecture:
The cheaper the storage, the longer you wait to get your data back.
This is not merely an inconvenience—it fundamentally changes how applications must be designed. A system that assumes all data is instantly accessible cannot suddenly handle 12-hour retrieval delays. The architectural implications of cold storage touch every layer of the stack.
The spectrum of retrieval latencies:
| Storage Tier | First Byte Latency | Cost Savings | Architectural Impact |
|---|---|---|---|
| Hot (Standard) | 5-50ms | Baseline | Synchronous, inline |
| Warm (IA) | 50-100ms | 40-50% | Slightly degraded UX |
| Cold (Glacier IR) | 50-100ms | 80% | Barely noticeable |
| Cold (Glacier Flexible) | 1-12 hours | 85% | Async workflows required |
| Archive (Deep Archive) | 12-48 hours | 95% | Batch/scheduled only |
This page explores how to navigate this spectrum—making smart tier placement decisions and designing applications that gracefully handle the full range of retrieval latencies.
This page covers the mechanics of cold storage retrieval, architectural patterns for async data access, strategies for balancing latency with cost, user experience design for delayed retrieval, and frameworks for making tier placement decisions based on latency tolerance.
Cold storage achieves its low cost by physically storing data differently than hot storage. Understanding these mechanics helps explain why retrieval is slow and what factors influence retrieval time.
How Archive Storage Works Physically:
In traditional archive storage (like AWS Glacier's original implementation), data is stored on high-density magnetic tape in robotic tape libraries. When you request data:
This mechanical process simply cannot be instant. Even modern archive implementations using dense HDD arrays or specialized archive infrastructure have overhead that precludes instant access.
| Retrieval Type | Time to First Byte | Per-GB Cost | Per-Request Cost | Best For |
|---|---|---|---|---|
| Expedited | 1-5 minutes | $0.03 | $10/1K requests | Urgent, small objects |
| Standard | 3-5 hours | $0.01 | $0.05/1K requests | Normal restore operations |
| Bulk | 5-12 hours | $0.0025 | $0.025/1K requests | Large-scale data migration |
Glacier Instant Retrieval vs Flexible Retrieval:
AWS introduced Glacier Instant Retrieval to address the common pattern where data is rarely accessed but, when needed, requires immediate availability (e.g., medical records accessed once a year but needed immediately in emergencies).
| Feature | Glacier Instant | Glacier Flexible | Deep Archive |
|---|---|---|---|
| Retrieval Latency | Milliseconds | Hours | 12-48 hours |
| Storage Cost | $0.004/GB | $0.0036/GB | $0.00099/GB |
| Retrieval Cost | $0.03/GB | $0.01-0.03/GB | $0.02-0.05/GB |
| Minimum Storage | 90 days | 90 days | 180 days |
| Use Case | Quarterly access | Annual access | Legal/compliance |
The architectural implications here are profound: Glacier Instant behaves like warm storage from an application perspective, while Glacier Flexible requires completely different handling.
Expedited retrievals from Glacier Flexible are not guaranteed—they're capacity-limited. During high-demand periods, expedited requests can fail. To guarantee expedited access, you must purchase Provisioned Capacity units ($100/month per unit, each supports 3 expedited requests per 5 minutes). For predictable urgent access needs, factor this into your architecture.
When data resides in Glacier Flexible or Deep Archive, you cannot read it directly. You must first restore the object—a process that creates a temporary copy in hot storage. This creates a two-phase access pattern that applications must explicitly handle.
The Restore Lifecycle:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
Object Restore Lifecycle═══════════════════════════════════════════════════════════════════════════ Phase 1: Object in Archive (Normal State)┌─────────────────────────────────────────────────────────────────────────┐│ Object: reports/2020/annual-report.pdf ││ Storage Class: GLACIER ││ Status: ARCHIVED ││ Access: ✗ Cannot read - must restore first │└─────────────────────────────────────────────────────────────────────────┘ │ │ Restore Request (RestoreObject API) │ Specify: retrieval tier (Standard/Expedited/Bulk) │ restore duration (days) ▼Phase 2: Restore In Progress┌─────────────────────────────────────────────────────────────────────────┐│ Object: reports/2020/annual-report.pdf ││ Storage Class: GLACIER ││ Status: RESTORE_IN_PROGRESS ││ Access: ✗ Cannot read - restore not yet complete ││ Estimated completion: ~3-5 hours (Standard tier) │└─────────────────────────────────────────────────────────────────────────┘ │ │ Restore completes (async, check via HeadObject) │ Or receive S3 Event Notification "Restore Completed" ▼Phase 3: Restored Copy Available┌─────────────────────────────────────────────────────────────────────────┐│ Object: reports/2020/annual-report.pdf ││ Storage Class: GLACIER (unchanged - archival copy remains) ││ Status: RESTORED ││ Access: ✓ Can read normally via GetObject ││ Temporary copy expires: In 7 days (as configured) ││ x-amz-restore header: ongoing-request="false", expiry-date="..." │└─────────────────────────────────────────────────────────────────────────┘ │ │ Expiration period elapses (configurable 1-30 days) │ Or: another restore extends expiration ▼Phase 4: Restored Copy Expires┌─────────────────────────────────────────────────────────────────────────┐│ Object: reports/2020/annual-report.pdf ││ Storage Class: GLACIER ││ Status: ARCHIVED (back to Phase 1) ││ Access: ✗ Cannot read - restore expired │└─────────────────────────────────────────────────────────────────────────┘Implementing Restore-Then-Access:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
import { S3Client, RestoreObjectCommand, HeadObjectCommand, GetObjectCommand} from '@aws-sdk/client-s3'; interface RestoreResult { status: 'already_restored' | 'restore_initiated' | 'restore_in_progress' | 'error'; estimatedCompletionTime?: Date; expirationTime?: Date;} async function restoreGlacierObject( bucket: string, key: string, restoreDays: number = 7, retrievalTier: 'Expedited' | 'Standard' | 'Bulk' = 'Standard'): Promise<RestoreResult> { const s3 = new S3Client({}); // First, check current restore status const headResponse = await s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key })); // Parse restore header if present const restoreHeader = headResponse.Restore; if (restoreHeader) { if (restoreHeader.includes('ongoing-request="true"')) { // Restore already in progress return { status: 'restore_in_progress' }; } if (restoreHeader.includes('ongoing-request="false"')) { // Object already restored; extract expiration const expiryMatch = restoreHeader.match(/expiry-date="([^"]+)"/); const expirationTime = expiryMatch ? new Date(expiryMatch[1]) : undefined; return { status: 'already_restored', expirationTime }; } } // Initiate restore try { await s3.send(new RestoreObjectCommand({ Bucket: bucket, Key: key, RestoreRequest: { Days: restoreDays, GlacierJobParameters: { Tier: retrievalTier } } })); // Estimate completion based on tier const estimatedHours = { 'Expedited': 0.1, // ~5 minutes 'Standard': 4, // 3-5 hours 'Bulk': 8 // 5-12 hours }; const estimatedCompletionTime = new Date( Date.now() + estimatedHours[retrievalTier] * 60 * 60 * 1000 ); return { status: 'restore_initiated', estimatedCompletionTime }; } catch (error: any) { if (error.name === 'RestoreAlreadyInProgressException') { return { status: 'restore_in_progress' }; } throw error; }} // Usage in applicationasync function accessArchivedReport(reportId: string): Promise<{ ready: boolean; url?: string; estimatedWait?: number;}> { const key = `reports/${reportId}.pdf`; const restoreResult = await restoreGlacierObject('my-bucket', key); switch (restoreResult.status) { case 'already_restored': // Data is ready - generate presigned URL const url = await generatePresignedUrl('my-bucket', key, 3600); return { ready: true, url }; case 'restore_initiated': case 'restore_in_progress': // Data not ready - inform user of wait time const waitMs = restoreResult.estimatedCompletionTime ? restoreResult.estimatedCompletionTime.getTime() - Date.now() : 4 * 60 * 60 * 1000; // Default 4 hours return { ready: false, estimatedWait: waitMs }; default: throw new Error('Failed to restore archived data'); }}Each restore request incurs retrieval costs based on object size and retrieval tier. Restoring 1TB from Deep Archive with Standard retrieval costs ~$20 in retrieval fees alone. Expedited retrieval costs 10x more. Factor these costs into your per-request economics.
When retrieval takes hours, synchronous request-response patterns break down. Applications accessing cold storage must adopt asynchronous patterns that decouple the request from the response.
Pattern 1: Request-Wait-Notify
The most common pattern for cold data access: user requests data, system initiates restore, user is notified when ready.
123456789101112131415161718192021222324252627282930313233343536
┌─────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────┐│ User │ │ API/App │ │ Job Queue │ │ S3 │└────┬────┘ └──────┬──────┘ └──────┬──────┘ └────┬────┘ │ │ │ │ │ 1. Request Report │ │ │ │────────────────────▶│ │ │ │ │ 2. Check storage class │ │ │────────────────────────────────────────────▶│ │ │ │ GLACIER │ │ │◀────────────────────────────────────────────│ │ │ 3. Initiate restore │ │ │ │────────────────────────────────────────────▶│ │ │ │ 202 Accepted │ │ │◀────────────────────────────────────────────│ │ │ 4. Queue notification job │ │ │──────────────────────▶│ │ │ 5. Response: "Request received, │ │ │ will notify when ready (~4 hours)" │ │ │◀────────────────────│ │ │ │ │ │ │ │ ~~~~~ 4 hours pass ~~~~~ │ │ │ │ │ │ │ │ 6. S3 Event: "Restore Completed" │ │ │◀──────────────────────────────────────────── │ │ │ 7. Pick up job │ │ │ │◀──────────────────────│ │ │ 8. Email/Push: "Your report is ready" │ │ │◀────────────────────│ │ │ │ │ │ │ │ 9. Access report │ │ │ │────────────────────▶│ │ │ │ │ 10. Get object │ │ │ │────────────────────────────────────────────▶│ │ Report PDF │ │ Report PDF │ │◀────────────────────│◀────────────────────────────────────────────│ │ │ │ │Pattern 2: Hot Cache with Cold Backing
Maintain frequently accessed data in a hot cache, with cold storage as the authoritative source. Cache misses trigger async restore, and the user can retry later.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
interface DataAccessResult { source: 'hot_cache' | 'warm_storage' | 'cold_pending'; data?: Buffer; estimatedAvailability?: Date;} class TieredDataAccessor { private hotCache: Map<string, { data: Buffer; expiry: Date }> = new Map(); private pendingRestores: Map<string, Date> = new Map(); async getData(key: string): Promise<DataAccessResult> { // Level 1: Check hot in-memory cache const cached = this.hotCache.get(key); if (cached && cached.expiry > new Date()) { return { source: 'hot_cache', data: cached.data }; } // Level 2: Check if restore is pending const pendingRestore = this.pendingRestores.get(key); if (pendingRestore) { return { source: 'cold_pending', estimatedAvailability: pendingRestore }; } // Level 3: Try to read from S3 (might be hot/warm/cold) try { const headResult = await this.headObject(key); // Check if object is in Glacier if (headResult.StorageClass?.includes('GLACIER') || headResult.StorageClass?.includes('DEEP_ARCHIVE')) { // Check if already restored if (this.isRestored(headResult.Restore)) { // Restored - fetch it const data = await this.getObject(key); this.cacheHot(key, data); return { source: 'warm_storage', data }; } // Not restored - initiate restore and track const estimatedReady = await this.initiateRestore(key); this.pendingRestores.set(key, estimatedReady); return { source: 'cold_pending', estimatedAvailability: estimatedReady }; } // Object is in hot/warm storage - read directly const data = await this.getObject(key); this.cacheHot(key, data); return { source: 'warm_storage', data }; } catch (error) { throw new Error(`Failed to access data: ${key}`); } } private cacheHot(key: string, data: Buffer) { // Cache for 1 hour const expiry = new Date(Date.now() + 60 * 60 * 1000); this.hotCache.set(key, { data, expiry }); } private isRestored(restoreHeader?: string): boolean { return restoreHeader?.includes('ongoing-request="false"') ?? false; } async getOrWait(key: string, maxWaitMs: number = 60000): Promise<Buffer> { const result = await this.getData(key); if (result.data) { return result.data; } if (result.source === 'cold_pending' && result.estimatedAvailability) { const waitTime = result.estimatedAvailability.getTime() - Date.now(); if (waitTime > maxWaitMs) { throw new Error( `Data not available. ` + `Estimated availability: ${result.estimatedAvailability.toISOString()}` ); } // Poll until ready or timeout return this.pollUntilReady(key, maxWaitMs); } throw new Error('Data access failed'); }}S3 can send notifications when restore completes via SNS, SQS, or Lambda. Configure 's3:ObjectRestore:Completed' events instead of polling. This is more efficient and provides near-instant notification when data becomes available.
The best way to handle cold storage latency is to avoid it entirely by restoring data before it's needed. Predictive prefetching uses patterns in data access to proactively restore likely-to-be-accessed data.
Prefetch Strategies:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
interface PrefetchRule { id: string; name: string; triggerType: 'schedule' | 'access' | 'event'; trigger: ScheduleTrigger | AccessTrigger | EventTrigger; prefetchTargets: PrefetchTarget[]; costBudgetPerMonth: number; // Maximum restore cost enabled: boolean;} interface ScheduleTrigger { cronExpression: string; // e.g., "0 0 1 * *" for monthly} interface AccessTrigger { objectPattern: string; // e.g., "reports/2024/*" relatedPattern: string; // e.g., "reports/2023/*" } interface EventTrigger { eventType: 'user_login' | 'navigation' | 'search' | 'custom'; matchCriteria: Record<string, string>;} interface PrefetchTarget { objectPattern: string; limit?: number; // Max objects to prefetch restoreDays: number; retrievalTier: 'Expedited' | 'Standard' | 'Bulk';} class PredictivePrefetcher { private prefetchRules: PrefetchRule[]; private monthlyBudgetUsed: number = 0; async handleAccessEvent(accessedKey: string): Promise<void> { // Find rules triggered by this access const triggeredRules = this.prefetchRules.filter(rule => rule.enabled && rule.triggerType === 'access' && this.matchesPattern(accessedKey, (rule.trigger as AccessTrigger).objectPattern) ); for (const rule of triggeredRules) { await this.executePrefetch(rule); } } async handleScheduledPrefetch(ruleId: string): Promise<void> { const rule = this.prefetchRules.find(r => r.id === ruleId); if (rule) { await this.executePrefetch(rule); } } private async executePrefetch(rule: PrefetchRule): Promise<void> { for (const target of rule.prefetchTargets) { // List objects matching pattern that are in Glacier const glacierObjects = await this.findGlacierObjects(target.objectPattern); // Limit to target.limit const limitedObjects = glacierObjects.slice(0, target.limit); // Estimate cost const estimatedCost = await this.estimateRestoreCost( limitedObjects, target.retrievalTier ); // Check budget if (this.monthlyBudgetUsed + estimatedCost > rule.costBudgetPerMonth) { console.log(`Skipping prefetch for ${rule.name}: budget exceeded`); continue; } // Execute restores for (const obj of limitedObjects) { await this.initiateRestore(obj.key, target.restoreDays, target.retrievalTier); console.log(`Prefetched: ${obj.key}`); } this.monthlyBudgetUsed += estimatedCost; await this.recordPrefetchMetric(rule.id, limitedObjects.length, estimatedCost); } }} // Example rulesconst prefetchRules: PrefetchRule[] = [ { id: 'quarterly-reports', name: 'Pre-restore quarterly reports before earnings', triggerType: 'schedule', trigger: { cronExpression: '0 0 1 1,4,7,10 *' // 1st of Jan, Apr, Jul, Oct }, prefetchTargets: [{ objectPattern: 'reports/quarterly/*', restoreDays: 14, retrievalTier: 'Standard' }], costBudgetPerMonth: 100, enabled: true }, { id: 'related-year-prefetch', name: 'When accessing 2024, prefetch 2023', triggerType: 'access', trigger: { objectPattern: 'documents/2024/*', relatedPattern: 'documents/2023/*' }, prefetchTargets: [{ objectPattern: 'documents/2023/*', limit: 50, restoreDays: 7, retrievalTier: 'Standard' }], costBudgetPerMonth: 200, enabled: true }];Aggressive prefetching can consume significant budget if not controlled. Always set cost limits and monitor prefetch hit rates. If prefetched data is rarely accessed, you're paying retrieval costs for nothing. Track the ratio of 'prefetch hits' to 'prefetch total' and disable low-hit-rate rules.
When users must wait hours for data, user experience design becomes critical. Poor UX leads to frustrated users, support tickets, and pressure to abandon cold storage entirely. Good UX makes delayed retrieval acceptable—even expected.
UX Principles for Delayed Data Access:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
interface ArchiveAccessUIState { status: 'available' | 'archived' | 'restoring' | 'restored' | 'error'; estimatedReadyTime?: Date; progress?: number; // 0-100 notificationPreference?: 'email' | 'push' | 'sms' | 'none'; alternativeContent?: { type: 'thumbnail' | 'summary' | 'metadata'; content: any; };} // React component example (conceptual)function ArchiveDataAccess({ dataId, state }: { dataId: string; state: ArchiveAccessUIState; }) { return ( <div className="archive-access-container"> {state.status === 'available' && ( <Button onClick={() => downloadData(dataId)}> Download Now </Button> )} {state.status === 'archived' && ( <div className="archive-notice"> <InfoIcon /> <p>This file is in long-term storage.</p> <p>Retrieval typically takes 3-5 hours.</p> <div className="notification-preferences"> <label>Notify me when ready:</label> <Select options={['Email', 'Push Notification', 'SMS', 'No notification']} defaultValue="Email" /> </div> <Button onClick={() => initiateRestore(dataId)}> Request File </Button> </div> )} {state.status === 'restoring' && ( <div className="restore-progress"> <Spinner /> <p>Retrieving from archive...</p> {state.estimatedReadyTime && ( <p> Estimated ready: {formatRelativeTime(state.estimatedReadyTime)} </p> )} {state.progress && ( <ProgressBar value={state.progress} /> )} {state.alternativeContent && ( <div className="alternative-content"> <p>While you wait, here's a preview:</p> <AlternativePreview content={state.alternativeContent} /> </div> )} </div> )} {state.status === 'restored' && ( <div className="restore-complete"> <SuccessIcon /> <p>Your file is ready!</p> <p className="expiry-notice"> Available for the next 7 days. </p> <Button onClick={() => downloadData(dataId)}> Download Now </Button> </div> )} {state.status === 'error' && ( <div className="restore-error"> <ErrorIcon /> <p>Unable to retrieve this file.</p> <Button onClick={() => retryRestore(dataId)}> Try Again </Button> <Button variant="secondary" onClick={contactSupport}> Contact Support </Button> </div> )} </div> );}Label archived content clearly throughout your UI. A folder icon with a clock or snowflake can indicate archived status at a glance. Users who understand content is archived before clicking will have appropriate expectations. Surprise is the enemy of good UX.
The fundamental question of tier placement is: How long can this data's users wait? Different data types and use cases have drastically different latency tolerances, and tier selection must align with these requirements.
Latency Tolerance Categories:
| Category | Max Wait | User Context | Appropriate Tiers | Examples |
|---|---|---|---|---|
| Interactive | <500ms | User waiting in UI | Standard only | App assets, session data |
| Near-Inline | <5s | Loading indicator acceptable | Standard, IA | Dashboard data, reports |
| Short-Wait | <5min | User can do other tasks | IA, Glacier Instant | On-demand exports, batch results |
| Scheduled | Hours | Background processing | Glacier Flexible | Nightly jobs, analytics |
| Async | Days | Async notification acceptable | Deep Archive | Compliance, legal requests |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
Tier Selection Based on Latency Requirements═══════════════════════════════════════════════════════════════════════════ What is the maximum acceptable retrieval latency for users? │ ┌─────────────────────────┼─────────────────────────┐ ▼ ▼ ▼ Milliseconds Seconds/Minutes Hours/Days │ │ │ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ S3 STANDARD │ │ TIER BY │ │ GLACIER │ │ No choice │ │ FREQUENCY │ │ TIER BY │ │ │ │ │ │ URGENCY │ └──────────────┘ └───────┬──────┘ └───────┬──────┘ │ │ Access > 1x/week? Sometimes need │ urgent access? ┌───────YES─────┴─────NO───────┐ │ ▼ ▼ │ S3 STANDARD-IA Access > 1x/month? │ ($0.0125/GB) │ │ ┌────────YES────┴────NO──┤ ▼ ▼ GLACIER INSTANT ┌──────────────┐ ($0.004/GB) │ YES → GLACIER │ Millisecond access │ FLEXIBLE │ │ ($0.0036/GB) │ │ + Provisioned │ │ Capacity for │ │ expedited │ ├────────────────┤ │ NO → DEEP │ │ ARCHIVE │ │ ($0.00099/GB) │ │ Batch only │ └────────────────┘ OVERRIDE CONDITIONS:─────────────────────────────────────────────────────────────────────────────• Regulatory requirement for rapid access → Use Glacier Instant maximum• Object size < 128KB → Stay in Standard (tiering overhead not worthwhile)• Highly variable access patterns → Use S3 Intelligent-Tiering• Multi-year retention only → Deep Archive regardless of occasional access• Business-critical & unpredictable → Standard (cost of latency > storage)The Cost of Wrong Tier Placement:
Misaligned tier placement creates either unnecessary cost or unacceptable user experience:
If you're uncertain about access patterns, S3 Intelligent-Tiering removes the guesswork. It monitors access and automatically moves objects between tiers without retrieval fees. The small monitoring fee is insurance against tier misplacement.
Complex applications often need hybrid approaches that combine multiple strategies to handle the full spectrum of access patterns.
Pattern: Tiered Fallback with Progressive Enhancement
Serve what's available immediately; enhance as more data becomes accessible:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
interface DocumentViewState { phase: 'loading' | 'metadata' | 'preview' | 'full'; metadata?: DocumentMetadata; preview?: { thumbnail?: string; firstPageText?: string; summary?: string; }; fullContent?: Buffer; archiveRestoreStatus?: { initiated: boolean; estimatedReady?: Date; };} async function* loadDocumentProgressive( documentId: string): AsyncGenerator<DocumentViewState> { // Phase 1: Show loading state immediately yield { phase: 'loading' }; // Phase 2: Load metadata (always hot, instant) const metadata = await loadMetadata(documentId); yield { phase: 'metadata', metadata }; // Phase 3: Load preview from hot cache/CDN const preview = await loadPreviewFromCache(documentId); if (preview) { yield { phase: 'preview', metadata, preview }; } // Phase 4: Attempt to load full content const storageInfo = await getStorageInfo(documentId); if (storageInfo.tier === 'STANDARD' || storageInfo.tier === 'STANDARD_IA') { // Full content is accessible const fullContent = await downloadContent(documentId); yield { phase: 'full', metadata, preview, fullContent }; } else if (storageInfo.tier.includes('GLACIER')) { // Content is archived const restoreStatus = await checkOrInitiateRestore(documentId); if (restoreStatus.ready) { // Already restored - download it const fullContent = await downloadContent(documentId); yield { phase: 'full', metadata, preview, fullContent }; } else { // Still restoring - return preview with restore status yield { phase: 'preview', metadata, preview, archiveRestoreStatus: { initiated: true, estimatedReady: restoreStatus.estimatedReady } }; // Optionally: continue polling and yield when ready // (In practice, you'd handle this via notifications) } }} // Usage in a React componentfunction DocumentViewer({ documentId }: { documentId: string }) { const [state, setState] = useState<DocumentViewState>({ phase: 'loading' }); useEffect(() => { const loader = loadDocumentProgressive(documentId); (async () => { for await (const viewState of loader) { setState(viewState); } })(); return () => loader.return(undefined); }, [documentId]); return ( <div> {state.phase === 'loading' && <Spinner />} {state.metadata && <MetadataHeader metadata={state.metadata} />} {state.preview && <PreviewPane preview={state.preview} />} {state.fullContent && <FullDocumentViewer content={state.fullContent} />} {state.archiveRestoreStatus && ( <ArchiveNotice estimatedReady={state.archiveRestoreStatus.estimatedReady} onNotifyMe={(method) => setupNotification(documentId, method)} /> )} </div> );}Pattern: Emergency Override with Budget Guardrails
For systems where cold data can become urgent unexpectedly, implement emergency expedited retrieval with cost controls:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
interface EmergencyRetrievalBudget { monthlyBudgetUSD: number; usedThisMonth: number; alertThreshold: number; // Percentage to alert} async function emergencyRetrieve( objectKey: string, userId: string, justification: string, budget: EmergencyRetrievalBudget): Promise<{ success: boolean; message: string }> { // Estimate cost const objectSize = await getObjectSize(objectKey); const estimatedCostUSD = (objectSize / (1024 ** 3)) * 0.03; // $0.03/GB expedited const totalAfterThis = budget.usedThisMonth + estimatedCostUSD; // Check budget if (totalAfterThis > budget.monthlyBudgetUSD) { // Over budget - reject or escalate await notifyAdmin({ type: 'emergency_retrieval_budget_exceeded', objectKey, userId, justification, estimatedCost: estimatedCostUSD, currentUsage: budget.usedThisMonth, monthlyBudget: budget.monthlyBudgetUSD }); return { success: false, message: 'Emergency retrieval budget exceeded. Request escalated to admin.' }; } // Hit alert threshold? const percentUsed = (totalAfterThis / budget.monthlyBudgetUSD) * 100; if (percentUsed > budget.alertThreshold) { await notifyAdmin({ type: 'emergency_retrieval_budget_warning', percentUsed, remainingBudget: budget.monthlyBudgetUSD - totalAfterThis }); } // Execute expedited retrieval try { await initiateRestore(objectKey, { tier: 'Expedited', days: 1 // Minimum restore duration }); // Log for audit await logEmergencyRetrieval({ objectKey, userId, justification, cost: estimatedCostUSD, timestamp: new Date() }); // Update budget tracking budget.usedThisMonth += estimatedCostUSD; await saveBudget(budget); return { success: true, message: 'Expedited retrieval initiated. Data available in 1-5 minutes.' }; } catch (error) { if (error.name === 'InsufficientCapacityException') { return { success: false, message: 'Expedited capacity unavailable. Using standard retrieval (3-5 hours).' }; } throw error; }}Always log emergency/expedited retrievals with user, justification, and cost. This creates accountability and helps identify if 'emergencies' are actually predictable patterns that should trigger prefetching or tier adjustment.
Retrieval time trade-offs are the final piece of the tiered storage puzzle. Understanding how cold storage works, designing applications for async access, and implementing strategies that mask latency from users enables aggressive cost optimization without sacrificing user experience.
Let's consolidate the essential principles:
Module Complete:
With this page, you've completed the Hot, Warm, and Cold Storage module. You now understand the complete lifecycle of tiered storage—from analyzing access patterns through implementing lifecycle policies, optimizing costs, and handling retrieval trade-offs. This knowledge enables you to build storage architectures that achieve massive cost savings while maintaining the performance characteristics your applications require.
You have mastered the art and science of tiered storage. From access pattern analysis to retrieval time trade-offs, you can now design storage architectures that optimize for cost, performance, and user experience. These skills are essential for any system handling data at scale.