Design Instagram: The Complete Photo Sharing System Design Guide — A Senior+ Guide
Instagram is one of the most widely used photo and video sharing social networks in the world, serving over two billion monthly active users and more than five hundred million daily active users. The platform processes over one hundred million photo and video uploads every single day, delivers tens of billions of feed reads daily, and handles real-time direct messaging for hundreds of millions of conversations simultaneously. Building and maintaining such a system requires solving some of the most challenging problems in distributed systems engineering: media upload and transcoding at massive scale, feed generation with fanout strategies that balance consistency and latency, content discovery through machine learning recommendation pipelines, ephemeral content delivery for Stories, real-time bidirectional messaging, and multi-tier caching that keeps p99 latencies under fifty milliseconds even during peak traffic events.
This guide is written for senior and staff-level engineers who are preparing for system design interviews at top technology companies or who are architecting media-heavy platforms in production. We will dissect every major subsystem of Instagram from first principles, walk through capacity estimation, choose appropriate data stores, design the APIs, and implement the critical paths in C#. Each section includes architecture diagrams rendered as Mermaid, detailed HTML tables comparing trade-offs, and production-grade C# code blocks that demonstrate real implementation patterns rather than pseudocode abstractions.
1. Functional and Non-Functional Requirements
Before designing any distributed system, we must clearly define what the system must do and how it must behave under load. Instagram's requirements span media management, social graph operations, content delivery, and real-time features. Getting the requirements right prevents scope creep during the interview and ensures the architecture addresses the actual problem rather than a simplified approximation.
Functional Requirements
- Upload Photo/Video: Users can upload photos (JPEG, PNG, HEIC) and videos (MP4, MOV) up to 60 minutes in length. The system must accept the upload, store the original, and generate multiple resized variants asynchronously.
- View Feed: Users see a chronological or ranked feed of posts from accounts they follow. The feed must load in under 200 milliseconds at the p95.
- Like, Comment, Save: Users can like a post, leave comments, and save posts to collections. Like counts must be eventually consistent within two seconds.
- Follow/Unfollow: Users can follow and unfollow other accounts. The social graph must support both fanout-on-write and fanout-on-read patterns.
- Explore/Discovery: Users can browse a personalized Explore page that recommends content from accounts they do not follow.
- Stories: Users can post ephemeral photos and videos that disappear after 24 hours. Story viewers are tracked for analytics.
- Direct Messaging: Users can send text, photos, videos, and share posts in one-on-one and group conversations. Messages must be delivered in real-time.
- Reels: Short-form video content with an algorithmic distribution model.
- Notifications: Push and in-app notifications for likes, comments, follows, mentions, and direct messages.
- User Profiles: Users have a profile with bio, profile picture, post grid, and follower/following counts.
Non-Functional Requirements
| Requirement | Target | Rationale |
|---|---|---|
| Availability | 99.99% (52 min downtime/year) | Social media platforms must be always accessible |
| Latency (Feed Read) | p95 < 200ms | Users abandon slow feeds |
| Latency (Upload) | Upload acknowledged < 500ms | Processing is async, but upload confirmation must be instant |
| Consistency | Eventual consistency for feeds, strong for messages | Feed can lag by a few seconds; messages cannot be lost |
| Durability | 99.999999999% (11 nines) for media | Photos are irreplaceable user content |
| Throughput | 500K+ peak QPS for feed reads | Traffic spikes during major events and time zones |
2. Capacity Estimation and Back-of-Envelope Math
Capacity estimation is the foundation of every system design answer. It drives your choices of data stores, replication factors, cache sizes, and sharding strategies. The numbers below are based on publicly available data about Instagram's infrastructure and reasonable extrapolations from Meta's engineering blog posts.
User and Traffic Estimates
| Metric | Value | Calculation |
|---|---|---|
| Monthly Active Users (MAU) | 2.5 billion | Public figure from Meta earnings reports |
| Daily Active Users (DAU) | 600 million | ~24% of MAU (typical social network DAU/MAU ratio) |
| Photos uploaded per day | 100 million | 100M DAU upload rate with varying frequencies |
| Videos uploaded per day | 50 million | Growing video share on the platform |
| Average photo size (compressed) | 2 MB | JPEG quality 85, 1080px wide |
| Average video size (compressed) | 15 MB | H.264 720p, 30 seconds average |
| Feed reads per user per day | 25 | Session-based with pull-to-refresh behavior |
| Average followers per user | 200 | Power law: most users follow 50-500 accounts |
| Likes per day | 4.5 billion | ~45 likes per upload on average |
| Comments per day | 600 million | ~6 comments per upload on average |
Storage Estimation
Daily photo storage: 100 million uploads times 2 MB average equals 200 TB per day of raw photo data. When we generate four size variants (thumbnail at 150x150 at 20 KB, small at 640x640 at 100 KB, medium at 1080x1080 at 500 KB, and the original at 2 MB), the effective storage multiplies to roughly 800 TB per day for photos alone.
Daily video storage: 50 million uploads times 15 MB average equals 750 TB per day. Video transcoding generates three variants (360p, 720p, 1080p), but since the originals are already compressed, the total multiplier is approximately 2x, giving us 1.5 PB per day for videos.
Combined daily storage: approximately 2.3 PB per day. Over a year this amounts to roughly 840 PB. In practice, lifecycle policies archive older content to cheaper storage tiers (S3 Glacier), reducing active storage to roughly 50 PB at any given time.
Bandwidth Estimation
Upload bandwidth: 200 TB of photos plus 750 TB of videos equals 950 TB per day. That is roughly 950 trillion bytes divided by 86,400 seconds, which is about 8.8 Gbps average upload bandwidth, peaking at roughly 35 Gbps during peak hours.
Download bandwidth: 600 million DAU times 25 feed reads per day times 50 posts per feed times 200 KB average per post equals 150 TB per day for feed reads alone. Adding Explore page, profile views, Stories, and Reels, total download bandwidth approaches 500 TB per day, or about 4.6 Gbps average and 20+ Gbps peak.
C#
// Capacity estimation helper
public class CapacityEstimator
{
public const long DailyPhotoUploads = 100_000_000;
public const long DailyVideoUploads = 50_000_000;
public const long AveragePhotoSizeBytes = 2_000_000;
public const long AverageVideoSizeBytes = 15_000_000;
public const int ThumbnailVariants = 4;
public const int VideoVariants = 3;
public static long DailyPhotoStorageBytes =>
DailyPhotoUploads * AveragePhotoSizeBytes * ThumbnailVariants;
public static long DailyVideoStorageBytes =>
DailyVideoUploads * AverageVideoSizeBytes * VideoVariants;
public static long DailyTotalStorageBytes =>
DailyPhotoStorageBytes + DailyVideoStorageBytes;
public static double DailyStoragePetabytes =>
DailyTotalStorageBytes / (1024.0 * 1024 * 1024 * 1024 * 1024);
public static long FeedReadsPerDay(long dau, int readsPerUser, int postsPerFeed) =>
dau * readsPerUser * postsPerFeed;
public static double AverageFeedQPS(long dailyReads) =>
dailyReads / 86_400.0;
public static double PeakFeedQPS(long dailyReads) =>
AverageFeedQPS(dailyReads) * 4.0;
public static void PrintReport()
{
Console.WriteLine($"Daily Photo Storage: {DailyPhotoStorageBytes / (1024.0*1024*1024*1024):F1} TB");
Console.WriteLine($"Daily Video Storage: {DailyVideoStorageBytes / (1024.0*1024*1024*1024):F1} TB");
Console.WriteLine($"Daily Total Storage: {DailyStoragePetabytes:F1} PB");
Console.WriteLine($"Feed Reads/Day: {FeedReadsPerDay(600_000_000, 25, 50):N0}");
Console.WriteLine($"Avg Feed QPS: {AverageFeedQPS(FeedReadsPerDay(600_000_000, 25, 50)):N0}");
Console.WriteLine($"Peak Feed QPS: {PeakFeedQPS(FeedReadsPerDay(600_000_000, 25, 50)):N0}");
}
}
3. High-Level Architecture Overview
Instagram's architecture follows a microservices pattern where each major feature is an independent service communicating over gRPC and asynchronous message queues. The key architectural principle is separation of the read path from the write path, allowing each to be scaled, cached, and optimized independently.
Layer 7] LB --> APIGateway[API Gateway
Rate Limiting, Auth] APIGateway --> UploadService[Upload Service] APIGateway --> FeedService[Feed Service] APIGateway --> ExploreService[Explore Service] APIGateway --> StoryService[Story Service] APIGateway --> DMService[DM Service] APIGateway --> SearchService[Search Service] APIGateway --> NotificationService[Notification Service] UploadService --> MediaQueue[Kafka
Media Processing] MediaQueue --> ImageProcessor[Image Processor] MediaQueue --> VideoProcessor[Video Transcoder] ImageProcessor --> S3[S3 Media Bucket] VideoProcessor --> S3 ImageProcessor --> CDN[CloudFront CDN] VideoProcessor --> CDN ImageProcessor --> MetadataStore[Cassandra
Media Metadata] FeedService --> FanoutService[Fanout Service] FanoutService --> FeedCache[Redis Cluster
Feed Cache] FanoutService --> SocialGraphDB[PostgreSQL
Social Graph] ExploreService --> MLRanker[ML Ranker
TensorFlow] MLRanker --> CandidateStore[Candidate Store
Redis] StoryService --> StoryCache[Redis
Active Stories] StoryService --> S3 DMService --> MessageDB[Cassandra
Messages] DMService --> PresenceService[Presence Service
Redis] DMService --> WebSocket[WebSocket Gateway
MQTT] NotificationService --> PushQueue[Kafka
Notifications] PushQueue --> APNS[Apple Push] PushQueue --> FCM[Firebase Cloud Messaging] SearchService --> ElasticSearch[Elasticsearch Cluster]
The API Gateway is the single entry point for all client requests. It handles authentication via JWT tokens, rate limiting per user, request routing to the appropriate microservice, and response compression. The gateway is stateless and horizontally scalable behind a Layer 7 load balancer that performs TLS termination and sticky sessions based on user ID.
Each service owns its data store and communicates with other services only through well-defined APIs or message queues. This eliminates tight coupling and allows teams to deploy independently. The trade-off is eventual consistency, which is acceptable for most Instagram features except direct messaging, where we use a synchronous write path with a fallback to async replication.
Key Design Decisions
| Decision | Choice | Why |
|---|---|---|
| API Protocol | gRPC (internal), REST (external) | gRPC is efficient for service-to-service; REST for client compatibility |
| Message Broker | Apache Kafka | High throughput, durability, replay capability for media pipeline |
| Primary Database | PostgreSQL + Cassandra | PostgreSQL for relational data; Cassandra for high-write workloads |
| Cache Layer | Redis Cluster | Sorted sets for feeds, pub/sub for real-time, TTL for expiry |
| Media Storage | S3 + CloudFront CDN | Durability of 11 nines, global edge delivery |
| Search Engine | Elasticsearch | Full-text search, geospatial queries, autocomplete |
| Real-Time | MQTT over WebSocket | Lightweight protocol, persistent connections, QoS levels |
4. Photo and Video Upload Pipeline
The upload pipeline is the most critical write path in Instagram. A poorly designed upload pipeline leads to data loss, corrupted images, or slow processing that frustrates users. The pipeline must handle binary media uploads of varying sizes, validate content, generate multiple derivatives, and make the content available on the CDN within seconds of the initial upload.
Upload Flow Step by Step
- Client Request: The mobile app uses a resumable upload protocol. It first requests an upload session from the upload service, receives a pre-signed S3 URL, and begins a multipart upload directly to S3. This bypasses the application server for the actual bytes, reducing memory pressure.
- Upload Acknowledgment: Once all parts are uploaded, the client sends a completion request to the upload service. The service validates the upload (checksum, file size, format) and returns a mediaId to the client. The upload is now acknowledged and the user sees their post in their feed.
- Async Processing: The upload service publishes a MediaUploaded event to Kafka. Multiple consumer groups process this event in parallel: image processing, video transcoding, NSFW detection, metadata indexing, and feed fanout.
- Image Processing: The image processor downloads the original from S3, generates four variants (thumbnail, small, medium, original), and uploads them to the CDN-backed S3 bucket. Each variant is optimized using the Sharp library with progressive JPEG encoding and EXIF orientation correction.
- Video Transcoding: The video transcoder uses FFmpeg to produce three resolution variants (360p, 720p, 1080p) in H.264 codec. Thumbnail frames are extracted at 1-second, 3-second, and 5-second intervals for the video preview.
- Metadata Update: After processing completes, a metadata update event is published. The metadata service updates the Cassandra row for the media with the CDN URLs, processing status, dimensions, file size, and content hash for duplicate detection.
- Feed Fanout: The fanout service reads the metadata update and inserts the media ID into the feed caches of all followers. This step is described in detail in the Feed Generation section.
C#
// Upload Service - handles the initial upload and triggers async processing
public class UploadController : ControllerBase
{
private readonly IS3Client _s3;
private readonly IKafkaProducer _kafka;
private readonly IMediaRepository _mediaRepo;
private readonly IChecksumValidator _checksum;
[HttpPost("api/v1/media/upload")]
public async Task<ActionResult<UploadResponse>> InitiateUpload(
[FromBody] UploadRequest request)
{
var mediaId = Guid.NewGuid().ToString("N");
var uploadSession = new UploadSession
{
MediaId = mediaId,
UserId = GetUserId(),
ContentType = request.ContentType,
FileSize = request.FileSize,
CreatedAt = DateTime.UtcNow,
Status = UploadStatus.Pending
};
var presignedUrl = await _s3.GeneratePresignedUploadUrl(
bucket: "instagram-raw-uploads",
key: $"uploads/{mediaId}/{request.FileName}",
expiration: TimeSpan.FromMinutes(30),
contentType: request.ContentType);
await _mediaRepo.CreateUploadSession(uploadSession);
return Ok(new UploadResponse
{
MediaId = mediaId,
UploadUrl = presignedUrl,
ExpiresAt = uploadSession.CreatedAt.AddMinutes(30)
});
}
[HttpPost("api/v1/media/{mediaId}/complete")]
public async Task<ActionResult> CompleteUpload(
string mediaId,
[FromBody] CompletionRequest request)
{
var session = await _mediaRepo.GetUploadSession(mediaId);
if (session == null) return NotFound();
var s3Object = await _s3.GetObjectAsync(
"instagram-raw-uploads", $"uploads/{mediaId}/{request.FileName}");
var isValid = await _checksum.Validate(
s3Object.Content, request.ExpectedChecksum);
if (!isValid)
{
session.Status = UploadStatus.Failed;
await _mediaRepo.UpdateUploadSession(session);
return BadRequest(new { error = "Checksum mismatch" });
}
var media = new Media
{
Id = mediaId,
UserId = session.UserId,
Type = DetectMediaType(request.ContentType),
OriginalUrl = $"s3://instagram-raw-uploads/uploads/{mediaId}/{request.FileName}",
Status = MediaStatus.Processing,
UploadedAt = DateTime.UtcNow,
Caption = request.Caption,
Location = request.Location,
AltText = request.AltText
};
await _mediaRepo.CreateMedia(media);
await _kafka.PublishAsync("media-uploaded", new MediaUploadedEvent
{
MediaId = mediaId,
UserId = session.UserId,
ContentType = request.ContentType,
S3Key = $"uploads/{mediaId}/{request.FileName}",
Timestamp = DateTime.UtcNow
});
session.Status = UploadStatus.Completed;
await _mediaRepo.UpdateUploadSession(session);
return Ok(new { mediaId, status = "processing" });
}
}
C#
// Image Processor - generates optimized variants
public class ImageProcessor : IKafkaConsumer<MediaUploadedEvent>
{
private readonly IS3Client _s3;
private readonly IKafkaProducer _kafka;
private static readonly ImageVariant[] Variants = new[]
{
new ImageVariant("thumbnail", 150, 150, FitMode.Cover, 80),
new ImageVariant("small", 640, 640, FitMode.Inside, 82),
new ImageVariant("medium", 1080, 1080, FitMode.Inside, 85),
new ImageVariant("original", 0, 0, FitMode.Keep, 90)
};
public async Task ConsumeAsync(MediaUploadedEvent evt)
{
if (!evt.ContentType.StartsWith("image/")) return;
var originalBytes = await _s3.GetObjectBytesAsync(
"instagram-raw-uploads", evt.S3Key);
foreach (var variant in Variants)
{
var processedBytes = ProcessImage(originalBytes, variant);
var cdnKey = $"media/{evt.MediaId}/{variant.Name}.jpg";
await _s3.PutObjectAsync(new PutObjectRequest
{
BucketName = "instagram-media-cdn",
Key = cdnKey,
InputStream = new MemoryStream(processedBytes),
ContentType = "image/jpeg",
CacheControl = "public, max-age=31536000"
});
}
await _kafka.PublishAsync("media-processed", new MediaProcessedEvent
{
MediaId = evt.MediaId,
UserId = evt.UserId,
ProcessingType = "image",
Success = true,
CdnBasePath = $"media/{evt.MediaId}",
CompletedAt = DateTime.UtcNow
});
}
private byte[] ProcessImage(byte[] original, ImageVariant variant)
{
using var image = Image.Load<Rgba32>(original);
if (variant.Width > 0 && variant.Height > 0)
{
image.Mutate(x => x.Resize(new ResizeOptions
{
Size = new Size(variant.Width, variant.Height),
Mode = variant.Fit == FitMode.Cover
? ResizeMode.Crop : ResizeMode.Max
}));
}
using var ms = new MemoryStream();
image.SaveAsJpeg(ms, new JpegEncoder
{
Quality = variant.Quality,
SkipMetadata = false
});
return ms.ToArray();
}
}
Resumable Upload Protocol
Mobile networks are unreliable. Instagram uses a custom resumable upload protocol inspired by the TUS standard. The client tracks which parts have been uploaded and can resume from the last successful part if the connection drops. Each part is 5 MB. The server tracks part completion in Redis with a 24-hour TTL. If the user resumes, the server returns the list of completed parts and the client continues from where it left off. This is critical for video uploads which can be hundreds of megabytes and take minutes on slow connections.
C#
// Resumable upload manager on the client side
public class ResumableUploadManager
{
private readonly HttpClient _http;
private const int PartSizeBytes = 5 * 1024 * 1024;
public async Task<string> UploadWithResume(
string uploadUrl, Stream fileStream, string mediaId)
{
var totalParts = (int)Math.Ceiling(fileStream.Length / (double)PartSizeBytes);
var completedParts = await GetCompletedParts(mediaId);
for (int partIndex = 0; partIndex < totalParts; partIndex++)
{
if (completedParts.Contains(partIndex)) continue;
var offset = partIndex * PartSizeBytes;
var length = Math.Min(PartSizeBytes, (int)(fileStream.Length - offset));
var buffer = new byte[length];
fileStream.Seek(offset, SeekOrigin.Begin);
await fileStream.ReadAsync(buffer, 0, length);
for (int retry = 0; retry < 3; retry++)
{
try
{
var content = new ByteArrayContent(buffer);
content.Headers.Add("Content-Range",
$"bytes {offset}-{offset + length - 1}/{fileStream.Length}");
var response = await _http.PutAsync(
$"{uploadUrl}/parts/{partIndex}", content);
response.EnsureSuccessStatusCode();
break;
}
catch (HttpRequestException) when (retry < 2)
{
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retry)));
}
}
}
var finalizeResponse = await _http.PostAsync(
$"{uploadUrl}/complete",
new StringContent(mediaId, Encoding.UTF8, "application/json"));
finalizeResponse.EnsureSuccessStatusCode();
return mediaId;
}
}
5. Media Storage and CDN Distribution
Instagram's media storage architecture must handle exabytes of data with extreme durability and deliver content globally with sub-100ms latency. The design uses a tiered storage approach: hot content is served from CDN edge locations, warm content lives in standard S3 with CloudFront, and cold content is archived to S3 Glacier Deep Archive.
Storage Tiers
| Tier | Storage | Access Pattern | Cost per GB/month | Retrieval Time |
|---|---|---|---|---|
| Hot (CDN Edge) | CloudFront Edge Locations | Top 1000 posts per user, recent posts | $0.085 | <20ms |
| Warm (Standard) | S3 Standard | All posts within 90 days | $0.023 | 50-200ms |
| Cool (Infrequent) | S3 Standard-IA | Posts 90-365 days old | $0.0125 | 100-500ms |
| Cold (Archive) | S3 Glacier Instant | Posts older than 1 year | $0.004 | 1-5 seconds |
| Deep Archive | S3 Glacier Deep Archive | Deleted accounts, legal holds | $0.00099 | 12-48 hours |
CDN cache invalidation is handled through versioned URLs rather than explicit invalidation requests. When a photo is edited or a policy change requires reprocessing, a new version suffix is appended to the URL (e.g., /media/abc123/medium.jpg?v=2), causing the CDN to treat it as a new resource without expensive invalidation operations.
C#
// Media URL resolver with CDN tier selection
public class MediaUrlResolver
{
private readonly ICloudFrontClient _cdn;
private readonly IS3Client _s3;
private readonly IMediaMetadataRepo _metadata;
public async Task<MediaUrls> ResolveUrls(string mediaId, string viewerRegion)
{
var metadata = await _metadata.GetMediaMetadata(mediaId);
var age = DateTime.UtcNow - metadata.UploadedAt;
MediaUrls urls = new();
foreach (var variant in new[] { "thumbnail", "small", "medium", "original" })
{
if (age.TotalDays < 90)
{
urls.Photos[variant] = _cdn.GetSignedUrl(
$"media/{mediaId}/{variant}.jpg",
region: viewerRegion,
expiration: TimeSpan.FromHours(1));
}
else if (age.TotalDays < 365)
{
urls.Photos[variant] = GetS3RedirectUrl(
"instagram-media-cdn",
$"media/{mediaId}/{variant}.jpg");
}
else
{
urls.Photos[variant] = await GenerateGlacierRedirectUrl(
$"media/{mediaId}/{variant}.jpg");
}
}
foreach (var resolution in new[] { "360p", "720p", "1080p" })
{
urls.Videos[resolution] = _cdn.GetSignedUrl(
$"media/{mediaId}/video/{resolution}.mp4",
region: viewerRegion,
expiration: TimeSpan.FromHours(1));
}
return urls;
}
}
Duplicate Detection
To prevent duplicate uploads and reduce storage costs, Instagram computes a perceptual hash (pHash) of each uploaded photo and a frame hash of video thumbnails. The hash is stored alongside the media metadata. Before processing a new upload, the system checks if a similar hash already exists within the same user's account. If a match is found with a similarity score above 95%, the upload is flagged as a potential duplicate and the user is notified. This prevents accidental re-uploads and intentional content theft detection.
6. Feed Generation and Fanout Strategy
Feed generation is arguably the most complex subsystem in Instagram. The feed must combine posts from followed accounts, ads, sponsored content, and recommended posts, all ranked by relevance and freshness. The fundamental architectural decision is between fanout-on-write (push model) and fanout-on-read (pull model), and Instagram uses a hybrid approach that combines the strengths of both.
Fanout-on-Write (Push Model)
When a regular user (fewer than 500,000 followers) publishes a post, the fanout service immediately inserts the media ID into the feed cache of every follower. The feed cache is a Redis sorted set keyed by feed:{userId} with the score being the post timestamp in microseconds. Each feed is capped at 800 entries using ZREMRANGEBYRANK after each insert.
The advantage of fanout-on-write is extremely fast feed reads. The feed is pre-computed and cached, so reading the feed is a single Redis ZREVRANGE call. The disadvantage is the write amplification: a user with 100,000 followers requires 100,000 Redis operations per post. For most users this is manageable, but celebrity accounts with millions of followers would create millions of cache writes per post, overwhelming the Redis cluster.
Fanout-on-Read (Pull Model)
For celebrity users (500,000+ followers), Instagram does not fanout their posts to followers' caches. Instead, their posts are stored in a separate celebrity_posts:{userId} Redis sorted set. At feed read time, the feed service fetches the user's pre-computed timeline from their cache and merges it with the most recent posts from celebrity accounts they follow. The merge uses a priority queue (min-heap) sorted by timestamp, and the top 50 results are returned.
Hybrid Strategy Comparison
| Aspect | Fanout-on-Write | Fanout-on-Read | Instagram Hybrid |
|---|---|---|---|
| Read Latency | Very low (cache hit) | Higher (merge at read) | Low (pre-computed + merge) |
| Write Amplification | High (N followers) | Zero | Controlled (threshold) |
| Feed Freshness | Excellent | Good | Excellent |
| Storage Overhead | High (per-user caches) | Low | Moderate |
| Celebrity Posts | Problematic | Native | Handled via pull |
| Cache Memory | ~320 GB (600M users) | ~2 GB | ~200 GB |
C#
// Hybrid Feed Generation Service
public class FeedService
{
private readonly IRedisCluster _redis;
private readonly IFollowGraphRepository _followGraph;
private readonly IMediaMetadataRepo _mediaRepo;
private const int FeedCap = 800;
private const int CelebrityThreshold = 500_000;
private const int PageSize = 20;
public async Task FeedFanoutOnWrite(string authorId, string mediaId, long timestamp)
{
var followerCount = await _followGraph.GetFollowerCount(authorId);
if (followerCount <= CelebrityThreshold)
{
string cursor = null;
do
{
var page = await _followGraph.GetFollowersPage(authorId, cursor, 10_000);
var batch = _redis.CreateBatch();
foreach (var followerId in page.FollowerIds)
{
var key = $"feed:{followerId}";
batch.SortedSetAddAsync(key, new SortedSetEntry(mediaId, timestamp));
batch.SortedSetRemoveRangeByRankAsync(key, 0, -(FeedCap + 1));
}
await batch.ExecuteAsync();
cursor = page.NextCursor;
}
while (cursor != null);
}
else
{
var key = $"celebrity_posts:{authorId}";
await _redis.SortedSetAddAsync(key, new SortedSetEntry(mediaId, timestamp));
await _redis.SortedSetRemoveRangeByRankAsync(key, 0, -(FeedCap + 1));
}
}
public async Task<List<FeedItem>> GetFeed(string userId, string cursor = null, int count = PageSize)
{
long maxScore = cursor != null
? ParseCursorToTimestamp(cursor)
: long.MaxValue;
var timelineEntries = await _redis.SortedSetRangeByRankWithScoresAsync(
$"feed:{userId}", 0, -1, Order.Descending);
var candidateMediaIds = new List<string>();
foreach (var entry in timelineEntries)
{
if (ParseTimestamp(entry.Score) <= maxScore)
{
candidateMediaIds.Add(entry.Element);
}
}
var followedCelebrities = await _followGraph.GetFollowedCelebrities(userId);
foreach (var celebrityId in followedCelebrities)
{
var celebrityPosts = await _redis.SortedSetRangeByRankWithScoresAsync(
$"celebrity_posts:{celebrityId}", 0, 9, Order.Descending);
foreach (var post in celebrityPosts)
{
if (ParseTimestamp(post.Score) <= maxScore)
{
candidateMediaIds.Add(post.Element);
}
}
}
var allMedia = await _mediaRepo.GetMediaBatch(candidateMediaIds);
var sortedMedia = allMedia
.OrderByDescending(m => m.UploadedAt)
.Take(count)
.ToList();
var feedItems = new List<FeedItem>();
foreach (var media in sortedMedia)
{
var authorProfile = await GetUserProfileCached(media.UserId);
var likeCount = await GetLikeCountCached(media.Id);
var commentCount = await GetCommentCountCached(media.Id);
feedItems.Add(new FeedItem
{
Media = media,
Author = authorProfile,
LikeCount = likeCount,
CommentCount = commentCount,
HasLiked = await HasUserLiked(userId, media.Id),
Cursor = EncodeTimestampToCursor(media.UploadedAt)
});
}
return feedItems;
}
}
Feed Ranking
After merging pre-computed and celebrity posts, Instagram applies a machine learning ranking model. The model takes features like the user's historical engagement with the author, the post's engagement velocity, time since posting, content type preference of the viewer, and relationship strength. The model outputs a relevance score and the feed is re-sorted by this score. This is why Instagram no longer shows a purely chronological feed.
The ranking model is a gradient-boosted decision tree (LightGBM) served via a custom inference engine. Features are pre-computed in a Spark pipeline and stored in a feature store (Redis). The inference happens in the feed service with a p99 latency budget of 30 milliseconds. For users with very large followings, the model operates on the top 200 candidates rather than the full feed to maintain latency targets.
7. Database Schema Design
Instagram uses a polyglot persistence strategy: PostgreSQL for relational data with strong consistency requirements, Cassandra for high-write-throughput time-series data, Redis for caching and ephemeral state, and Elasticsearch for search. The schema design below covers the core tables required for the Instagram system.
PostgreSQL Schema (User and Social Graph)
SQL
-- Users table (partitioned by user_id hash)
CREATE TABLE users (
user_id BIGSERIAL PRIMARY KEY,
username VARCHAR(30) UNIQUE NOT NULL,
display_name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
bio VARCHAR(150),
profile_pic_url VARCHAR(500),
is_private BOOLEAN DEFAULT FALSE,
is_verified BOOLEAN DEFAULT FALSE,
follower_count INT DEFAULT 0,
following_count INT DEFAULT 0,
post_count INT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_users_username ON users (username);
CREATE INDEX idx_users_email ON users (email);
-- Follow relationships (bidirectional tracking)
CREATE TABLE follows (
follower_id BIGINT REFERENCES users(user_id),
followee_id BIGINT REFERENCES users(user_id),
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (follower_id, followee_id)
);
CREATE INDEX idx_follows_followee ON follows (followee_id);
-- Likes (composite key for fast dedup)
CREATE TABLE likes (
user_id BIGINT REFERENCES users(user_id),
media_id VARCHAR(64) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (user_id, media_id)
);
CREATE INDEX idx_likes_media ON likes (media_id);
-- Comments (threaded)
CREATE TABLE comments (
comment_id BIGSERIAL PRIMARY KEY,
media_id VARCHAR(64) NOT NULL,
user_id BIGINT REFERENCES users(user_id),
parent_id BIGINT REFERENCES comments(comment_id),
body TEXT NOT NULL,
like_count INT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_comments_media ON comments (media_id, created_at);
-- Blocks and mutes
CREATE TABLE blocks (
blocker_id BIGINT REFERENCES users(user_id),
blocked_id BIGINT REFERENCES users(user_id),
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (blocker_id, blocked_id)
);
CREATE TABLE mutes (
muter_id BIGINT REFERENCES users(user_id),
muted_id BIGINT REFERENCES users(user_id),
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (muter_id, muted_id)
);
Cassandra Schema (Media Metadata and Timelines)
CQL
-- Media metadata (high write throughput)
CREATE TABLE media (
media_id TEXT PRIMARY KEY,
user_id BIGINT,
media_type TEXT,
caption TEXT,
location_name TEXT,
location_lat DOUBLE,
location_lng DOUBLE,
thumbnail_url TEXT,
small_url TEXT,
medium_url TEXT,
original_url TEXT,
video_urls MAP<TEXT, TEXT>,
width INT,
height INT,
duration_sec INT,
file_size_bytes BIGINT,
like_count COUNTER,
comment_count COUNTER,
view_count COUNTER,
share_count COUNTER,
is_archived BOOLEAN,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
-- User timeline (materialized view for feed reads)
CREATE TABLE user_timeline (
user_id BIGINT,
media_id TEXT,
author_id BIGINT,
posted_at TIMESTAMP,
is_celebrity BOOLEAN,
PRIMARY KEY (user_id, posted_at, media_id)
) WITH CLUSTERING ORDER BY (posted_at DESC);
-- Story metadata
CREATE TABLE stories (
user_id BIGINT,
story_id TEXT,
media_url TEXT,
media_type TEXT,
created_at TIMESTAMP,
expires_at TIMESTAMP,
view_count COUNTER,
PRIMARY KEY (user_id, created_at, story_id)
) WITH CLUSTERING ORDER BY (created_at DESC);
-- Story views (append-only)
CREATE TABLE story_views (
user_id BIGINT,
story_id TEXT,
viewer_id BIGINT,
viewed_at TIMESTAMP,
PRIMARY KEY ((user_id, story_id), viewed_at, viewer_id)
) WITH CLUSTERING ORDER BY (viewed_at DESC);
-- Direct messages
CREATE TABLE messages (
conversation_id TEXT,
message_id TIMEUUID,
sender_id BIGINT,
message_type TEXT,
content TEXT,
media_url TEXT,
is_ephemeral BOOLEAN,
created_at TIMESTAMP,
PRIMARY KEY (conversation_id, message_id)
) WITH CLUSTERING ORDER BY (message_id DESC);
-- Conversation participants
CREATE TABLE conversations (
conversation_id TEXT,
participant_id BIGINT,
last_read_at TIMESTAMP,
is_muted BOOLEAN,
joined_at TIMESTAMP,
PRIMARY KEY (conversation_id, participant_id)
);
Sharding Strategy
PostgreSQL is sharded by user_id using consistent hashing. Each shard contains approximately 10 million users. The social graph tables (follows, blocks, mutes) are sharded by follower_id so that all relationships for a single user live on the same shard. Cross-shard joins are avoided by denormalizing the follower count into the users table and updating it asynchronously via a counter service.
Cassandra is naturally distributed by its partition key. For the media table, the partition key is media_id (UUID), which distributes writes evenly across nodes. For the user_timeline table, the partition key is user_id, which ensures that a user's timeline reads are served by a single partition. Cassandra handles the high write throughput of like increments, comment additions, and view tracking through its log-structured merge-tree storage engine.
8. Caching Architecture
Instagram's caching architecture is designed to serve the vast majority of reads from memory, keeping database load minimal. The system uses a three-tier cache: in-process LRU cache (L1), Redis cluster (L2), and CDN edge cache (L3 for media). Cache invalidation follows a write-through pattern for critical data and lazy invalidation with TTL for less critical data.
Cache Hierarchy
| Cache Level | Technology | Hit Rate | Latency | Use Case |
|---|---|---|---|---|
| L1 (In-Process) | ConcurrentDictionary with LRU eviction | ~40% of reads | <1ms | User profile, session, feature flags |
| L2 (Distributed) | Redis Cluster (6 nodes) | ~85% of reads | 2-5ms | Feed, like counts, story cache |
| L3 (CDN) | CloudFront (200+ edge locations) | ~90% of media reads | 10-30ms | Photos, videos, profile pictures |
| Database | PostgreSQL / Cassandra | Source of truth | 10-50ms | All data |
C#
// Three-tier cache manager
public class ThreeTierCacheManager<T> where T : class
{
private readonly ConcurrentDictionary<string, CacheEntry<T>> _l1Cache;
private readonly IDatabase _redis;
private readonly TimeSpan _l1Ttl = TimeSpan.FromSeconds(5);
private readonly TimeSpan _l2Ttl;
public ThreeTierCacheManager(IDatabase redis, TimeSpan l2Ttl)
{
_l1Cache = new ConcurrentDictionary<string, CacheEntry<T>>();
_redis = redis;
_l2Ttl = l2Ttl;
}
public async Task<T?> GetOrFetchAsync(
string key,
Func<Task<T?>> fetchFromDb)
{
if (_l1Cache.TryGetValue(key, out var l1Entry) &&
DateTime.UtcNow - l1Entry.Timestamp < _l1Ttl)
{
return l1Entry.Value;
}
var redisValue = await _redis.StringGetAsync($"cache:{key}");
if (redisValue.HasValue)
{
var l2Value = JsonSerializer.Deserialize<T>(redisValue);
_l1Cache[key] = new CacheEntry<T> { Value = l2Value, Timestamp = DateTime.UtcNow };
return l2Value;
}
var dbValue = await fetchFromDb();
if (dbValue != null)
{
var serialized = JsonSerializer.Serialize(dbValue);
await _redis.StringSetAsync($"cache:{key}", serialized, _l2Ttl);
_l1Cache[key] = new CacheEntry<T>
{
Value = dbValue,
Timestamp = DateTime.UtcNow
};
}
return dbValue;
}
public async Task InvalidateAsync(string key)
{
_l1Cache.TryRemove(key, out _);
await _redis.KeyDeleteAsync($"cache:{key}");
}
private class CacheEntry<V>
{
public V Value { get; set; }
public DateTime Timestamp { get; set; }
}
}
Cache Warming Strategy
Cache cold starts are dangerous for Instagram. When a server restarts or a new deployment rolls out, the L1 cache is empty. To mitigate this, Instagram uses cache warming: during deployment, each server makes background requests for the top 1000 most active users' profiles and feeds before it starts accepting production traffic. Additionally, the first request for any cache key triggers an asynchronous batch fetch that pre-populates the cache for related keys. For example, fetching a user profile also warms the cache for their last 10 posts and their follower count.
Cache stampede prevention is handled via distributed locks using Redis SETNX. When a cache miss occurs, the service acquires a lock before hitting the database. If another thread already holds the lock, the second thread waits briefly and retries the cache read. This prevents 10,000 simultaneous requests from all hitting the database when a popular post's cache expires.
C#
// Cache stampede prevention with distributed locking
public class StampedePreventionCache<T> where T : class
{
private readonly IDatabase _redis;
private readonly ThreeTierCacheManager<T> _cache;
public async Task<T?> GetSafelyAsync(
string key,
Func<Task<T?>> fetchFromDb,
TimeSpan? lockTimeout = null)
{
var lockTimeoutMs = (lockTimeout ?? TimeSpan.FromSeconds(5)).TotalMilliseconds;
var lockKey = $"lock:{key}";
var lockValue = Guid.NewGuid().ToString("N");
var cached = await _cache.GetOrFetchAsync(key, () => Task.FromResult<T?>(null));
if (cached != null) return cached;
var acquired = await _redis.StringSetAsync(
lockKey, lockValue, lockTimeoutMs, When.NotExists);
if (acquired)
{
try
{
return await _cache.GetOrFetchAsync(key, fetchFromDb);
}
finally
{
var currentValue = await _redis.StringGetAsync(lockKey);
if (currentValue == lockValue)
{
await _redis.KeyDeleteAsync(lockKey);
}
}
}
else
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(50);
var retry = await _cache.GetOrFetchAsync(
key, () => Task.FromResult<T?>(null));
if (retry != null) return retry;
}
return await fetchFromDb();
}
}
}
9. Explore Page and Recommendation Engine
The Instagram Explore page is one of the most complex ML-driven features in the platform. It serves as a personalized discovery surface where users find content from accounts they do not follow. The system processes billions of candidate posts and ranks them using deep learning models that predict engagement probability.
Recommendation Pipeline Architecture
Generation] --> B[Feature
Extraction] B --> C[First-Pass
Ranking] C --> D[Re-Ranking
Diversity] D --> E[Filtering
Blocks Seen] E --> F[Explore Grid
Output] A --- A1[Collaborative
Filtering] A --- A2[Content-Based
Filtering] A --- A3[Trending
Posts] C --- C1[LightGBM
Model]
The pipeline has five stages. First, candidate generation produces up to five thousand candidate media IDs from four sources: collaborative filtering (users who liked similar content), content-based filtering (images with similar visual features extracted by a CNN), trending posts in the user's geographic region, and top-performing posts from accounts similar to those the user follows.
Second, feature extraction computes features for each candidate: the viewer's historical engagement rate with the author, the post's engagement velocity (likes per hour since posting), the content type match (does the viewer prefer photos or videos), recency, and the viewer's interest category distribution. These features are fetched from the feature store in a single batch call.
Third, first-pass ranking applies a LightGBM model that predicts the probability the viewer will like, save, or share the post. The model is trained on historical engagement data with a training window of 90 days. The top 500 candidates after ranking are passed to the next stage.
Fourth, re-ranking applies diversity constraints: no more than two consecutive posts from the same author, a mix of photos and videos, and avoidance of content categories the viewer has repeatedly scrolled past. The re-ranking uses a submodular optimization approach that maximizes diversity while preserving relevance.
Fifth, filtering removes posts from blocked or muted accounts, posts the viewer has already seen, and posts that have been flagged by content moderation. The final grid of 30-50 posts is returned to the client.
C#
// Explore recommendation engine
public class ExploreService
{
private readonly ICollaborativeFiltering _cf;
private readonly IContentBasedFiltering _cbf;
private readonly ITrendingService _trending;
private readonly IFeatureStore _features;
private readonly ILGBMInference _rankingModel;
private readonly IBlockMuteRepository _blockMute;
private readonly ISeenPostRepository _seenPosts;
public async Task<ExploreResult> GetExploreFeed(
string userId, string cursor, int count = 30)
{
var cfTask = _cf.GetCandidates(userId, 2000);
var cbfTask = _cbf.GetCandidates(userId, 1500);
var trendingTask = _trending.GetRegionalTrending(userId, 1000);
var topPostsTask = GetTopPostsFromSimilarAccounts(userId, 500);
await Task.WhenAll(cfTask, cbfTask, trendingTask, topPostsTask);
var allCandidates = (await cfTask)
.Concat(await cbfTask)
.Concat(await trendingTask)
.Concat(await topPostsTask)
.GroupBy(c => c.MediaId)
.Select(g => g.OrderByDescending(c => c.Score).First())
.Take(5000)
.ToList();
var mediaIds = allCandidates.Select(c => c.MediaId).ToList();
var featureMatrix = await _features.GetFeatureBatch(userId, mediaIds);
var rankedCandidates = allCandidates
.Zip(featureMatrix, (candidate, features) =>
{
var engagementScore = _rankingModel.Predict(features);
return new RankedCandidate
{
MediaId = candidate.MediaId,
AuthorId = candidate.AuthorId,
Score = engagementScore,
ContentType = candidate.ContentType
};
})
.OrderByDescending(r => r.Score)
.Take(500)
.ToList();
var diversified = ApplyDiversityConstraints(rankedCandidates, maxConsecutive: 2);
var blocked = await _blockMute.GetBlockedIds(userId);
var muted = await _blockMute.GetMutedIds(userId);
var seen = await _seenPosts.GetSeenPostIds(userId, 7);
var filtered = diversified
.Where(c => !blocked.Contains(c.AuthorId))
.Where(c => !muted.Contains(c.AuthorId))
.Where(c => !seen.Contains(c.MediaId))
.Take(count)
.ToList();
foreach (var item in filtered)
{
await _seenPosts.RecordImpression(userId, item.MediaId);
}
return new ExploreResult
{
Items = filtered,
NextCursor = filtered.Last()?.MediaId
};
}
private List<RankedCandidate> ApplyDiversityConstraints(
List<RankedCandidate> candidates, int maxConsecutive)
{
var result = new List<RankedCandidate>();
var authorStreak = new Dictionary<string, int>();
var typeStreak = 0;
string lastType = null;
foreach (var candidate in candidates)
{
var authorCount = authorStreak.GetValueOrDefault(candidate.AuthorId, 0);
if (authorCount >= maxConsecutive) continue;
if (candidate.ContentType == lastType)
{
typeStreak++;
if (typeStreak >= 3) continue;
}
else
{
typeStreak = 0;
lastType = candidate.ContentType;
}
result.Add(candidate);
authorStreak[candidate.AuthorId] = authorCount + 1;
}
return result;
}
}
Feature Store Design
The feature store is a critical component that pre-computes and caches ML features for real-time inference. It stores two types of features: user features (engagement history, content preferences, activity patterns) and post features (engagement velocity, visual features, author statistics). Features are refreshed every 15 minutes by a Spark Streaming job and stored in Redis with a 30-minute TTL. This ensures the ranking model always has fresh signals without the latency penalty of computing features at query time.
10. Instagram Stories Architecture
Instagram Stories represent ephemeral content that automatically expires after 24 hours. This feature requires special architectural considerations: content must be delivered quickly, viewed status must be tracked in real-time, and storage must be cleaned up automatically. The Stories system handles over 500 million daily viewers and billions of story views per day.
Stories Data Flow
High Priority] B --> C[Image Video Processor] C --> D[S3 Storage] C --> E[Redis Story Cache] end subgraph Story Viewing F[Story Tray Request] --> G[Story Service] G --> H{Followed User
Has Active Story} H -->|Yes| I[Return Story Metadata] H -->|No| J[Exclude from Tray] I --> K[Client Loads
from CDN] K --> L[Track View
Cassandra] end E --> F D --> K
When a user publishes a story, the upload service processes it with higher priority than regular posts (priority queue tier 1 vs tier 3). The image processor generates two variants: a full-resolution version and a compressed version optimized for mobile viewing. The processed story is stored in S3 with a 24-hour lifecycle policy, and the story metadata is inserted into the user's Redis story list with a matching TTL.
The story tray is computed at read time using a fanout-on-read approach. When a user opens Instagram, the client requests the story tray, which queries the list of followed users and checks Redis for active (non-expired) stories from each. The tray is ordered by recency, with unviewed stories first, then viewed stories. The tray is cached on the client for 60 seconds to reduce server load during rapid app interactions.
C#
// Story service implementation
public class StoryService
{
private readonly IDatabase _redis;
private readonly IS3Client _s3;
private readonly IStoryRepository _storyRepo;
private const int StoryTTLSeconds = 86400;
private const int MaxStoriesPerUser = 100;
public async Task<PublishStoryResult> PublishStory(
string userId, StoryUpload upload)
{
var processedMedia = await ProcessStoryMedia(upload);
var s3Key = $"stories/{userId}/{upload.StoryId}";
await _s3.PutObjectAsync(new PutObjectRequest
{
BucketName = "instagram-stories",
Key = s3Key,
InputStream = processedMedia.Stream,
ContentType = processedMedia.ContentType
});
var storyEntry = new StoryEntry
{
StoryId = upload.StoryId,
MediaType = upload.MediaType,
CdnUrl = $"https://cdn.instagram.com/{s3Key}",
CreatedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.AddHours(24)
};
var redisKey = $"stories:{userId}";
await _redis.ListLeftPushAsync(redisKey,
JsonSerializer.Serialize(storyEntry));
await _redis.ListTrimAsync(redisKey, 0, MaxStoriesPerUser - 1);
await _redis.KeyExpireAsync(redisKey,
TimeSpan.FromSeconds(StoryTTLSeconds));
return new PublishStoryResult
{
StoryId = upload.StoryId,
Status = "active"
};
}
public async Task<StoryTray> GetStoryTray(string viewerId)
{
var followees = await GetActiveFollowees(viewerId, limit: 100);
var trayItems = new List<StoryTrayItem>();
var tasks = followees.Select(async followeeId =>
{
var stories = await _redis.ListRangeAsync(
$"stories:{followeeId}", 0, -1);
if (stories.Length == 0) return null;
var lastViewed = await _redis.StringGetAsync(
$"story_viewed:{viewerId}:{followeeId}");
var entries = stories
.Select(s => JsonSerializer.Deserialize<StoryEntry>(s))
.Where(e => e.ExpiresAt > DateTime.UtcNow)
.OrderByDescending(e => e.CreatedAt)
.ToList();
if (entries.Count == 0) return null;
var hasUnviewed = !lastViewed.HasValue ||
entries.Any(e => e.CreatedAt > DateTime.Parse(lastViewed));
return new StoryTrayItem
{
UserId = followeeId,
HasUnviewed = hasUnviewed,
StoryCount = entries.Count,
LatestStory = entries.First()
};
});
var results = await Task.WhenAll(tasks);
return new StoryTray
{
Unviewed = results
.Where(r => r != null && r.HasUnviewed)
.OrderByDescending(r => r.LatestStory.CreatedAt)
.ToList(),
Viewed = results
.Where(r => r != null && !r.HasUnviewed)
.OrderByDescending(r => r.LatestStory.CreatedAt)
.ToList()
};
}
public async Task RecordStoryView(
string viewerId, string storyOwnerId, string storyId)
{
await _storyRepo.RecordView(new StoryView
{
UserId = storyOwnerId,
StoryId = storyId,
ViewerId = viewerId,
ViewedAt = DateTime.UtcNow
});
await _redis.StringSetAsync(
$"story_viewed:{viewerId}:{storyOwnerId}",
DateTime.UtcNow.ToString("O"),
TimeSpan.FromSeconds(StoryTTLSeconds));
await _redis.StringIncrementAsync($"story_views:{storyId}");
}
}
Story Highlights
Story Highlights are stories that users choose to preserve beyond the 24-hour window. When a user adds a story to a highlight, the media is copied from the stories S3 bucket to the permanent media bucket and associated with a highlight record in PostgreSQL. The highlight appears on the user's profile as a circular thumbnail that links to a curated collection of past stories. The architecture is simple: the copy operation is async, and the highlight metadata is stored in a relational table with a many-to-many relationship between highlights and stories.
11. Direct Messaging and Real-Time Communication
Instagram Direct Messaging supports one-on-one conversations, group chats up to 250 members, media sharing, post sharing, voice messages, and disappearing messages. The system handles billions of messages per day with real-time delivery, read receipts, and typing indicators. The architecture draws from WhatsApp's messaging system with modifications for Instagram's social graph.
Messaging Architecture
Messages] E --> G[Redis
Presence Typing] E --> H[PostgreSQL
Conversations] end subgraph Notification Layer E --> I[Push Notification
Service] I --> J[APNS FCM] end
The WebSocket Gateway maintains persistent connections with all online users. Each gateway node handles up to 100,000 concurrent WebSocket connections using an Erlang/OTP-based MQTT broker. When a user sends a message, the message is routed through the following path: (1) the client publishes to the MQTT topic dm/{conversationId}, (2) the MQTT broker routes the message to the message router, (3) the message router writes the message to Cassandra, (4) the message router publishes to the MQTT topics of all online participants, (5) if a participant is offline, the push notification service sends an APNS/FCM notification.
C#
// Direct Message service
public class DirectMessageService
{
private readonly ICassandraClient _cassandra;
private readonly IDatabase _redis;
private readonly IMqttBroker _mqtt;
private readonly IPushNotificationService _push;
private readonly IConversationRepository _conversations;
public async Task<SendMessageResult> SendMessage(SendMessageRequest request)
{
var conversationId = await GetOrCreateConversation(
request.SenderId, request.RecipientIds);
var messageId = TimeUuid.NewId();
var message = new Message
{
ConversationId = conversationId,
MessageId = messageId,
SenderId = request.SenderId,
MessageType = request.Type,
Content = request.Content,
MediaUrl = request.MediaUrl,
IsEphemeral = request.IsEphemeral,
CreatedAt = DateTime.UtcNow
};
await _cassandra.ExecuteAsync(
@"INSERT INTO messages (conversation_id, message_id, sender_id,
message_type, content, media_url, is_ephemeral, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
message.ConversationId, message.MessageId, message.SenderId,
message.MessageType, message.Content, message.MediaUrl,
message.IsEphemeral, message.CreatedAt);
await _conversations.UpdateLastMessage(conversationId, message);
var payload = JsonSerializer.Serialize(new MessagePayload
{
ConversationId = conversationId,
Message = message
});
foreach (var participantId in request.RecipientIds)
{
var isOnline = await _redis.KeyExistsAsync(
$"online:{participantId}");
if (isOnline)
{
await _mqtt.PublishAsync(
$"dm/{conversationId}/{participantId}", payload);
}
else
{
await _push.QueueNotificationAsync(new PushNotification
{
UserId = participantId,
Title = await GetSenderName(request.SenderId),
Body = request.Type == MessageType.Text
? request.Content
: $"Sent a {request.Type}",
Data = new Dictionary<string, string>
{
["conversationId"] = conversationId,
["messageId"] = messageId.ToString()
},
Priority = PushPriority.High
});
}
}
return new SendMessageResult
{
MessageId = messageId,
Status = MessageStatus.Sent
};
}
public async Task<List<Message>> GetConversationMessages(
string conversationId, TimeUuid? before, int limit = 50)
{
var query = before.HasValue
? @"SELECT * FROM messages WHERE conversation_id = ?
AND message_id < ? ORDER BY message_id DESC LIMIT ?"
: @"SELECT * FROM messages WHERE conversation_id = ?
ORDER BY message_id DESC LIMIT ?";
var parameters = before.HasValue
? new object[] { conversationId, before.Value, limit }
: new object[] { conversationId, limit };
var rows = await _cassandra.ExecuteAsync(query, parameters);
return rows.Select(MapToMessage).ToList();
}
public async Task MarkAsRead(
string userId, string conversationId, TimeUuid lastMessageId)
{
await _conversations.UpdateLastRead(
userId, conversationId, lastMessageId);
var participants = await _conversations.GetParticipants(conversationId);
foreach (var participantId in participants.Where(p => p != userId))
{
await _mqtt.PublishAsync(
$"dm/{conversationId}/{participantId}",
JsonSerializer.Serialize(new ReadReceipt
{
UserId = userId,
LastReadMessageId = lastMessageId,
Timestamp = DateTime.UtcNow
}));
}
}
}
Typing and Presence
Typing indicators and online presence are ephemeral features that use Redis with very short TTLs. When a user starts typing, the client publishes a typing_start event to the MQTT topic. The server stores a Redis key typing:{conversationId}:{userId} with a 5-second TTL. If the user continues typing, the client sends periodic heartbeat events that refresh the TTL. The other participants' clients poll for typing status every 2 seconds and display the typing indicator if the key exists.
Presence (online/offline status) uses the same Redis TTL mechanism. Each online user has a key online:{userId} with a 30-second TTL, refreshed every 15 seconds by the WebSocket gateway. When a user goes offline (WebSocket disconnects), the key expires and other users see them as offline after the TTL window. This approach avoids the complexity of tracking explicit offline events and handles ungraceful disconnects gracefully.
C#
// Presence and typing indicator service
public class PresenceService
{
private readonly IDatabase _redis;
private const int PresenceTTLSeconds = 30;
private const int TypingTTLSeconds = 5;
public async Task SetOnline(string userId)
{
await _redis.StringSetAsync(
$"online:{userId}", "1",
TimeSpan.FromSeconds(PresenceTTLSeconds));
}
public async Task RefreshPresence(string userId)
{
await _redis.KeyExpireAsync(
$"online:{userId}",
TimeSpan.FromSeconds(PresenceTTLSeconds));
}
public async Task SetOffline(string userId)
{
await _redis.KeyDeleteAsync($"online:{userId}");
}
public async Task<bool> IsOnline(string userId)
{
return await _redis.KeyExistsAsync($"online:{userId}");
}
public async Task SetTyping(string conversationId, string userId)
{
await _redis.StringSetAsync(
$"typing:{conversationId}:{userId}",
"1",
TimeSpan.FromSeconds(TypingTTLSeconds));
}
public async Task<List<string>> GetTypingUsers(
string conversationId, string excludeUserId)
{
var keys = await _redis.KeysAsync(
$"typing:{conversationId}:*");
return keys
.Select(k => k.ToString().Split(':').Last())
.Where(id => id != excludeUserId)
.ToList();
}
}
12. Search Infrastructure
Instagram's search system must support username search, hashtag search, location search, and full-text search across captions and comments. The system handles hundreds of thousands of queries per second with sub-100ms latency and provides autocomplete suggestions as the user types.
Search Components
| Search Type | Backend | Indexing Strategy | Latency Target |
|---|---|---|---|
| Username Autocomplete | Redis Trie + Elasticsearch | Trie in memory for top 1M usernames, ES for full search | <10ms (trie), <50ms (ES) |
| Hashtag Search | Elasticsearch | Inverted index on hashtags extracted from captions | <50ms |
| Location Search | Elasticsearch + PostGIS | Geospatial index on location coordinates | <80ms |
| Caption Full-Text | Elasticsearch | Analyzed text index with stemming and synonyms | <100ms |
| People You May Know | Custom Graph Service | Two-hop traversal of social graph | <200ms |
C#
// Search service with autocomplete
public class SearchService
{
private readonly IElasticClient _elastic;
private readonly ITrieIndex _usernameTrie;
private readonly IDatabase _redis;
public async Task<SearchResult> Search(string userId, string query,
SearchType type, int page = 1, int pageSize = 20)
{
await RecordSearch(userId, query);
return type switch
{
SearchType.Users => await SearchUsers(query, page, pageSize),
SearchType.Tags => await SearchHashtags(query, page, pageSize),
SearchType.Locations => await SearchLocations(query, page, pageSize),
SearchType.All => await SearchAll(userId, query, page, pageSize),
_ => throw new ArgumentException($"Unknown search type: {type}")
};
}
public async Task<List<AutocompleteResult>> Autocomplete(
string userId, string prefix, int limit = 10)
{
var trieResults = _usernameTrie.Search(prefix, limit * 2);
var blocked = await GetBlockedIds(userId);
var filtered = trieResults
.Where(r => !blocked.Contains(r.UserId))
.Take(limit)
.ToList();
if (filtered.Count < limit)
{
var esResults = await SearchUsers(prefix, 1, limit - filtered.Count);
filtered.AddRange(esResults.Users
.Where(u => !filtered.Any(f => f.UserId == u.UserId))
.Select(u => new AutocompleteResult
{
UserId = u.UserId,
Username = u.Username,
DisplayName = u.DisplayName,
ProfilePicUrl = u.ProfilePicUrl,
IsVerified = u.IsVerified
}));
}
return filtered;
}
private async Task<SearchResult> SearchUsers(
string query, int page, int pageSize)
{
var response = await _elastic.SearchAsync<UserDocument>(s =>
s.Index("users")
.From((page - 1) * pageSize)
.Size(pageSize)
.Query(q => q
.Bool(b => b
.Should(
sh => sh.Prefix(p =>
p.Field("username").Value(query).Boost(2.0f)),
sh => sh.MatchPhrase(m =>
m.Field("display_name").Query(query).Boost(1.5f)),
sh => sh.Fuzzy(f =>
f.Field("username").Value(query)
.Fuzziness(Fuzziness.Auto))
)
.MinimumShouldMatch(1)))
.Highlight(h => h
.PreTags("<mark>")
.PostTags("</mark>")
.Fields(
fi => fi.Field("username"),
fi => fi.Field("display_name"))));
return new SearchResult
{
Users = response.Documents.ToList(),
TotalHits = response.Total,
Page = page,
PageSize = pageSize
};
}
}
Search Indexing Pipeline
When a user creates an account or updates their profile, a Kafka event triggers the search indexing pipeline. The pipeline extracts the username, display name, bio, and verified status, then upserts the document into Elasticsearch. The index is partitioned by the first letter of the username for even distribution. Hashtag indexing happens in the media processing pipeline: when a photo caption contains a hashtag, the hashtag is extracted using a regex pattern and indexed as a separate document with a reference to the media ID. This allows efficient aggregation of all posts with a given hashtag.
13. Notifications System
Instagram's notification system must handle millions of notifications per minute across push (mobile), email, and in-app channels. The system must be reliable (no lost notifications), efficient (batching and deduplication), and respect user preferences (notification settings per type).
Notification Architecture
Likes Comments
Follows Mentions] --> B[Notification
Aggregator] B --> C[Deduplication
Batching] C --> D[Fanout] D --> E[Push Service
APNS FCM] D --> F[In-App Service
WebSocket] D --> G[Email Service
SES] B --> H[User Preferences
Redis] C --> I[Rate Limiter
Redis]
The notification flow begins when an event source (like, comment, follow, mention) publishes an event to the notification-events Kafka topic. The notification aggregator consumes these events and applies several processing steps before delivery.
First, the aggregator checks user notification preferences stored in Redis. If the user has disabled push notifications for likes, a like notification is suppressed. Second, the aggregator deduplicates notifications: if User A likes three photos by User B within 5 minutes, a single aggregated notification is generated instead of three separate notifications. Third, the rate limiter ensures no user receives more than 50 push notifications per hour, using a sliding window counter in Redis.
C#
// Notification aggregation and delivery
public class NotificationAggregator : IKafkaConsumer<NotificationEvent>
{
private readonly IDatabase _redis;
private readonly INotificationRepository _repo;
private readonly IPushService _push;
private readonly IWebSocketHub _websocket;
private static readonly TimeSpan DedupWindow = TimeSpan.FromMinutes(5);
private const int MaxPushPerHour = 50;
public async Task ConsumeAsync(NotificationEvent evt)
{
var preferences = await GetUserPreferences(evt.TargetUserId);
if (!IsNotificationEnabled(preferences, evt.Type)) return;
var dedupKey = $"notif_dedup:{evt.TargetUserId}:{evt.Type}:{evt.ActorId}:{evt.EntityType}";
var existing = await _redis.StringGetAsync(dedupKey);
if (existing.HasValue)
{
var aggregated = JsonSerializer.Deserialize<AggregatedNotification>(existing);
aggregated.Count++;
aggregated.ActorIds.TryAdd(evt.ActorId, await GetActorName(evt.ActorId));
aggregated.UpdatedAt = DateTime.UtcNow;
await _redis.StringSetAsync(dedupKey,
JsonSerializer.Serialize(aggregated), DedupWindow);
await _repo.UpdateNotification(aggregated.NotificationId, aggregated);
await SendRealTime(evt.TargetUserId, aggregated);
return;
}
var notification = new Notification
{
Id = Guid.NewGuid().ToString("N"),
TargetUserId = evt.TargetUserId,
Type = evt.Type,
ActorId = evt.ActorId,
ActorName = await GetActorName(evt.ActorId),
EntityType = evt.EntityType,
EntityId = evt.EntityId,
Body = BuildNotificationBody(evt),
CreatedAt = DateTime.UtcNow,
IsRead = false,
Count = 1
};
await _repo.CreateNotification(notification);
var aggregatedState = new AggregatedNotification
{
NotificationId = notification.Id,
Count = 1,
ActorIds = new Dictionary<string, string>
{
[evt.ActorId] = notification.ActorName
}
};
await _redis.StringSetAsync(dedupKey,
JsonSerializer.Serialize(aggregatedState), DedupWindow);
await SendRealTime(evt.TargetUserId, notification);
if (preferences.PushEnabled &&
evt.Type != NotificationType.Like &&
await CanSendPush(evt.TargetUserId))
{
await _push.SendAsync(new PushMessage
{
UserId = evt.TargetUserId,
Title = notification.ActorName,
Body = notification.Body,
Badge = await GetUnreadCount(evt.TargetUserId),
Data = new Dictionary<string, string>
{
["type"] = evt.Type.ToString(),
["entityId"] = evt.EntityId
}
});
}
}
private async Task<bool> CanSendPush(string userId)
{
var key = $"push_rate:{userId}";
var count = await _redis.StringIncrementAsync(key);
if (count == 1)
{
await _redis.KeyExpireAsync(key, TimeSpan.FromHours(1));
}
return count <= MaxPushPerHour;
}
private string BuildNotificationBody(NotificationEvent evt)
{
return evt.Type switch
{
NotificationType.Like => "liked your post.",
NotificationType.Comment => $"commented: \"{evt.CommentPreview}\"",
NotificationType.Follow => "started following you.",
NotificationType.Mention => "mentioned you in a comment.",
NotificationType.Tag => "tagged you in a post.",
_ => "interacted with your content."
};
}
}
Notification Preferences
Users can configure notification preferences per channel (push, email, in-app) and per type (likes, comments, follows, direct messages, story reactions, live videos). The preferences are stored in PostgreSQL and cached in Redis. The preference model supports per-sender muting (mute notifications from a specific user without blocking them) and quiet hours (no push notifications between 11 PM and 7 AM local time). The notification aggregator checks preferences before processing each notification, ensuring that unwanted notifications are filtered at the earliest possible stage.
14. Observability, Monitoring, and Incident Response
A platform serving two billion users cannot rely on manual monitoring. Instagram's observability stack provides real-time visibility into every subsystem through structured logging, distributed tracing, metrics collection, and automated alerting.
Monitoring Stack
| Component | Technology | Purpose | Retention |
|---|---|---|---|
| Metrics | Prometheus + Thanos | Time-series metrics, SLI/SLO tracking | 90 days hot, 1 year cold |
| Logging | ELK Stack | Structured logs, error analysis | 30 days |
| Tracing | Jaeger (OpenTelemetry) | Distributed trace visualization | 7 days |
| Alerting | Prometheus Alertmanager + PagerDuty | Automated alert routing | N/A |
| Dashboards | Grafana | Real-time visualization | N/A |
| Error Tracking | Sentry | Exception aggregation, stack traces | 30 days |
C#
// SLI metrics collection using OpenTelemetry
public class MetricsCollector
{
private readonly Meter _meter;
private readonly Counter<long> _feedReads;
private readonly Counter<long> _uploads;
private readonly Histogram<double> _feedLatency;
private readonly Histogram<double> _uploadLatency;
public MetricsCollector()
{
_meter = new Meter("instagram-service", "1.0.0");
_feedReads = _meter.CreateCounter<long>(
"instagram.feed.reads.total",
description: "Total feed read requests");
_uploads = _meter.CreateCounter<long>(
"instagram.upload.total",
description: "Total upload requests");
_feedLatency = _meter.CreateHistogram<double>(
"instagram.feed.latency.ms",
unit: "ms",
description: "Feed generation latency in milliseconds");
_uploadLatency = _meter.CreateHistogram<double>(
"instagram.upload.latency.ms",
unit: "ms",
description: "Upload completion latency in milliseconds");
}
public void RecordFeedRead(string userId, double latencyMs, bool cacheHit)
{
_feedReads.Add(1,
new KeyValuePair<string, object>("user_region", GetUserRegion(userId)));
_feedLatency.Record(latencyMs,
new KeyValuePair<string, object>("cache_hit", cacheHit));
}
public void RecordUpload(string userId, double latencyMs, string contentType)
{
_uploads.Add(1,
new KeyValuePair<string, object>("content_type", contentType));
_uploadLatency.Record(latencyMs);
}
}
Key Alert Rules
| Alert Name | Condition | Severity | Response |
|---|---|---|---|
| FeedLatencyHigh | p95 feed latency > 200ms for 5 min | Critical | Check Redis health, verify CDN cache hit rate |
| UploadFailureRate | Upload failure rate > 1% for 5 min | Warning | Check S3 connectivity, verify media processor health |
| CacheHitRateLow | Cache hit rate below 80% | Warning | Check Redis memory usage, review cache TTL settings |
| MessageDeliveryLag | Message delivery lag > 5 seconds | Critical | Check WebSocket gateway, verify MQTT broker health |
| KafkaConsumerLag | Consumer lag > 100K messages | Warning | Scale consumer group, check partition distribution |
| DiskUsageHigh | Disk usage > 85% | Warning | Verify lifecycle policies, check for stuck processes |
Incident Response Process
When a critical alert fires, the following automated process triggers: (1) PagerDuty creates an incident and pages the on-call engineer. (2) A Slack incident channel is created automatically with relevant metrics and recent deploy information. (3) The on-call engineer assesses severity and decides whether to mitigate (rollback, scale up) or escalate. (4) For P0 incidents affecting more than 1% of users, an incident commander is assigned and a war room is opened. (5) Post-incident, a blameless post-mortem is conducted within 48 hours and action items are tracked to completion.
15. Security, Privacy, and Content Moderation
Security and privacy are paramount for a platform handling billions of personal photos and private messages. Instagram's security architecture covers authentication, authorization, data encryption, content moderation, and regulatory compliance across multiple jurisdictions including GDPR, CCPA, and COPPA.
Authentication Flow
Instagram uses a two-token JWT system. The access token has a 15-minute expiry and is used for API authentication. The refresh token has a 30-day expiry and is stored in a secure HTTP-only cookie on web or encrypted SharedPreferences on mobile. When the access token expires, the client silently refreshes using the refresh token. The refresh token is single-use: each refresh rotates the token, invalidating the previous one. If a refresh token is compromised and reused, all tokens for that session are revoked and the user is logged out on all devices.
C#
// Authentication service
public class AuthenticationService
{
private readonly IUserRepository _users;
private readonly IDatabase _redis;
private readonly IJwtIssuer _jwt;
private readonly IPasswordHasher _hasher;
public async Task<AuthResult> Login(string username, string password)
{
var user = await _users.GetByUsername(username);
if (user == null || !_hasher.Verify(password, user.PasswordHash))
{
await RecordFailedAttempt(username);
throw new UnauthorizedAccessException("Invalid credentials");
}
if (await IsAccountLocked(user.UserId))
{
throw new UnauthorizedAccessException("Account temporarily locked");
}
var accessToken = _jwt.GenerateAccessToken(new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString()),
new Claim(ClaimTypes.Name, user.Username),
new Claim("verified", user.IsVerified.ToString())
}));
var refreshToken = _jwt.GenerateRefreshToken();
var refreshHash = _hasher.Hash(refreshToken);
await _redis.StringSetAsync(
$"refresh_token:{user.UserId}:{refreshToken}",
refreshHash,
TimeSpan.FromDays(30));
return new AuthResult
{
AccessToken = accessToken,
RefreshToken = refreshToken,
ExpiresAt = DateTime.UtcNow.AddMinutes(15),
UserId = user.UserId
};
}
public async Task<AuthResult> RefreshToken(string refreshToken)
{
var claims = _jwt.DecodeRefreshToken(refreshToken);
var userId = claims.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var storedHash = await _redis.StringGetAsync(
$"refresh_token:{userId}:{refreshToken}");
if (!storedHash.HasValue)
{
await RevokeAllTokens(userId);
throw new SecurityException("Refresh token reuse detected");
}
await _redis.KeyDeleteAsync(
$"refresh_token:{userId}:{refreshToken}");
var newAccessToken = _jwt.GenerateAccessToken(claims);
var newRefreshToken = _jwt.GenerateRefreshToken();
await _redis.StringSetAsync(
$"refresh_token:{userId}:{newRefreshToken}",
_hasher.Hash(newRefreshToken),
TimeSpan.FromDays(30));
return new AuthResult
{
AccessToken = newAccessToken,
RefreshToken = newRefreshToken,
ExpiresAt = DateTime.UtcNow.AddMinutes(15),
UserId = long.Parse(userId)
};
}
}
Content Moderation Pipeline
Every uploaded photo and video passes through an automated content moderation pipeline before being published. The pipeline uses a combination of machine learning classifiers and human review. The ML classifiers detect NSFW content (nudity, violence, gore), spam, copyright violations (using perceptual hash matching against a database of known copyrighted images), and text toxicity (for captions and comments). Content flagged as high-confidence violations is automatically removed. Content flagged as medium-confidence is placed in a human review queue with a 4-hour SLA.
Data Privacy Controls
Users can set their account to private, which restricts post visibility to approved followers only. The private account check is enforced at the API gateway level. Users can download all their data via the Download Your Data feature, which generates a ZIP archive containing all posts, messages, profile information, and search history within 48 hours. The Delete Account feature triggers a 30-day grace period during which the account is hidden but data is preserved, after which all data is permanently deleted from all storage tiers including backups.
16. Deployment, CI/CD, and Disaster Recovery
Instagram deploys code changes multiple times per day using a sophisticated CI/CD pipeline that ensures zero-downtime deployments and rapid rollback capability. The deployment strategy is canary-based, where new code is gradually rolled out to an increasing percentage of users while monitoring for errors and performance degradation.
Deployment Pipeline
Push] --> B[GitHub Actions
CI] B --> C[Build and
Unit Tests] C --> D[Docker
Build] D --> E[Integration
Tests] E --> F[Canary
Deploy 1%] F --> G{Monitor
5 min} G -->|Healthy| H[Canary 10%] H --> I{Monitor
15 min} I -->|Healthy| J[Canary 50%] J --> K{Monitor
30 min} K -->|Healthy| L[Full Rollout] G -->|Errors| M[Auto Rollback] I -->|Errors| M K -->|Errors| M
The CI pipeline runs unit tests, integration tests, and static analysis in parallel. Docker images are built with multi-stage builds for minimal image size. The canary deployment uses Kubernetes with Istio service mesh for traffic splitting. At each stage, the monitoring system watches for error rate increases, latency degradation, and SLO violations. If any anomaly is detected, the deployment is automatically rolled back within 60 seconds.
C#
// Health check and readiness probe
public class HealthCheckController : ControllerBase
{
private readonly IRedisCluster _redis;
private readonly ICassandraClient _cassandra;
private readonly IPgsqlDatabase _postgres;
private readonly IKafkaCluster _kafka;
[HttpGet("/health")]
public async Task<ActionResult<HealthStatus>> HealthCheck()
{
var checks = new List<HealthCheckResult>();
try
{
var sw = Stopwatch.StartNew();
await _redis.PingAsync();
sw.Stop();
checks.Add(new HealthCheckResult("redis", "healthy", sw.ElapsedMilliseconds));
}
catch (Exception ex)
{
checks.Add(new HealthCheckResult("redis", "unhealthy", error: ex.Message));
}
try
{
var sw = Stopwatch.StartNew();
await _cassandra.ExecuteAsync("SELECT now()");
sw.Stop();
checks.Add(new HealthCheckResult("cassandra", "healthy", sw.ElapsedMilliseconds));
}
catch (Exception ex)
{
checks.Add(new HealthCheckResult("cassandra", "unhealthy", error: ex.Message));
}
try
{
var sw = Stopwatch.StartNew();
await _postgres.ExecuteScalarAsync("SELECT 1");
sw.Stop();
checks.Add(new HealthCheckResult("postgresql", "healthy", sw.ElapsedMilliseconds));
}
catch (Exception ex)
{
checks.Add(new HealthCheckResult("postgresql", "unhealthy", error: ex.Message));
}
var overallStatus = checks.All(c => c.Status == "healthy")
? "healthy" : "degraded";
return Ok(new HealthStatus
{
Status = overallStatus,
Checks = checks,
Timestamp = DateTime.UtcNow,
Version = Assembly.GetExecutingAssembly().GetName().Version.ToString()
});
}
}
Backup and Recovery
PostgreSQL databases are backed up continuously using WAL (Write-Ahead Log) streaming to a standby server in a different availability zone. Full backups are taken weekly and stored in S3 Glacier. Point-in-time recovery is available for the last 30 days. Cassandra uses its native replication with a replication factor of 3 across different data centers. Cross-datacenter replication ensures that the loss of an entire data center does not result in data loss. Recovery Time Objective (RTO) is 15 minutes for database recovery and Recovery Point Objective (RPO) is 5 seconds for PostgreSQL (WAL lag) and near-zero for Cassandra (multi-DC replication).
17. Cost Estimation and Capacity Planning
Understanding the infrastructure cost of Instagram helps frame the scale of the system and informs architectural trade-offs. The cost estimates below are based on AWS pricing as of 2026 and represent a simplified model of Instagram's actual infrastructure costs.
Monthly Cost Breakdown
| Service | Configuration | Monthly Cost (Est.) |
|---|---|---|
| S3 Storage (50 PB active) | S3 Standard + lifecycle policies | $1,150,000 |
| CloudFront CDN | 500 TB transfer/month | $42,500 |
| EC2 Instances (App Servers) | 500 x c6i.4xlarge (on-demand mix) | $600,000 |
| ElastiCache Redis | 50 nodes x r6g.xlarge | $80,000 |
| Amazon MSK (Kafka) | 50 brokers x kafka.m5.2xlarge | $100,000 |
| RDS PostgreSQL | 20 instances x db.r6g.2xlarge (Multi-AZ) | $150,000 |
| OpenSearch (Elasticsearch) | 100 nodes x r6g.xlarge.search | $120,000 |
| Data Transfer | Inter-region + internet egress | $200,000 |
| ML Inference (SageMaker) | GPU instances for ranking model | $80,000 |
| Monitoring | CloudWatch, Datadog, Sentry | $50,000 |
| Other Services | SES, SQS, Lambda, etc. | $30,000 |
| Total Estimated | ~$2.6M/month |
These costs would be significantly higher for a real Instagram-scale platform, but Meta achieves substantial savings through custom silicon (MTIA chips for inference), massive volume discounts, and efficient resource utilization. The actual infrastructure cost per user is estimated at $0.001 per monthly active user, which is subsidized by advertising revenue averaging $4 per user per quarter.
Cost Optimization Strategies
- S3 Intelligent-Tiering: Automatically moves objects between access tiers based on usage patterns, saving 30-40% on storage costs for infrequently accessed content.
- Spot Instances for Batch Processing: Media processing, ML training, and analytics workloads run on spot instances at 60-70% discount. Only latency-sensitive services use on-demand or reserved instances.
- Right-Sizing: Continuous monitoring of CPU and memory utilization ensures instances are not over-provisioned. Automated scaling policies adjust capacity based on traffic patterns.
- CDN Caching Efficiency: Optimizing cache hit rates above 95% reduces origin fetches and associated data transfer costs significantly.
- Compression: All API responses use Brotli or gzip compression, reducing bandwidth consumption by 60-80% for text-based responses.
18. Interview Questions and Answers
The following are the most commonly asked Instagram system design interview questions at FAANG companies. Each answer is structured with a concise summary followed by the key architectural decisions and trade-offs.
Q1: How does Instagram store billions of photos?
Instagram stores photos in Amazon S3 with CloudFront CDN for global delivery. Each photo upload generates four size variants (thumbnail, small, medium, original) stored in separate S3 keys. Media metadata (dimensions, file size, upload time, author) is stored in Cassandra for high write throughput, while the user-post relationship is in PostgreSQL for relational queries. S3 lifecycle policies automatically transition older content to cheaper storage tiers: Standard for 0-90 days, Standard-IA for 90-365 days, Glacier Instant for 1-2 years, and Glacier Deep Archive for legal retention. CDN caching with 1-year TTLs ensures that popular content is served from edge locations with sub-20ms latency. Duplicate detection via perceptual hashing prevents redundant storage of identical images.
Q2: How does Instagram generate the feed?
Instagram uses a hybrid fanout model. For regular users with fewer than 500,000 followers, the platform uses fanout-on-write: when a post is published, the media ID is immediately inserted into every follower's feed cache in Redis. The feed cache is a Redis sorted set with timestamp as the score, capped at 800 entries per user. For celebrity users with more than 500,000 followers, the platform uses fanout-on-read: their posts are stored in a separate Redis set and merged with the user's pre-computed timeline at read time. This avoids the write amplification problem where a celebrity post would need to be pushed to millions of followers' caches. The feed service then applies a machine learning ranking model (LightGBM) that predicts engagement probability based on user features, post features, and relationship strength.
Q3: How does the Explore page recommendation system work?
The Explore page uses a multi-stage recommendation pipeline. Candidate generation produces up to 5,000 candidates from four sources: collaborative filtering, content-based filtering (visually similar images via CNN embeddings), trending posts, and top posts from similar accounts. Feature extraction computes engagement signals for each candidate. First-pass ranking applies a LightGBM model predicting engagement probability, reducing candidates to 500. Re-ranking applies diversity constraints (no consecutive posts from same author, mix of content types). Final filtering removes blocked and muted users and previously seen posts. The entire pipeline runs in under 200ms using pre-computed features from a feature store and batch inference.
Q4: How do Instagram Stories disappear after 24 hours?
Stories use a multi-layer expiration strategy. At the storage layer, S3 lifecycle policies automatically delete story objects after 24 hours. At the caching layer, Redis stores active story metadata with a 24-hour TTL that automatically expires the keys. At the application layer, the story service checks the expiration timestamp before serving any story and excludes expired stories from the story tray. The story tray itself uses fanout-on-read: at load time, the client requests story availability for the top 50 followed users, and only active (non-expired) stories from non-viewed users are shown first. Story Highlights are the exception: when a user saves a story to a highlight, the media is copied to permanent storage.
Q5: How does Instagram handle real-time direct messages?
DMs use MQTT over WebSocket connections for real-time delivery. Each user maintains a persistent WebSocket connection to the nearest gateway node, which handles up to 100,000 concurrent connections. When a message is sent, it is written to Cassandra (for durability) and published to the MQTT topic for the conversation. Online recipients receive the message in real-time through their WebSocket connection. Offline recipients receive push notifications via APNS or FCM. Read receipts and typing indicators use ephemeral Redis keys with 5-second TTLs that are refreshed periodically. The message sync across devices uses a sequence number approach where each conversation maintains a monotonically increasing message ID that clients use to request only new messages.
Q6: How would you design the upload pipeline for 100 million photos per day?
The upload pipeline uses a resumable upload protocol where the client uploads directly to S3 via pre-signed URLs, bypassing the application server for the actual binary transfer. After upload, the client confirms completion and the server validates the checksum. An async processing pipeline using Kafka handles image processing (Sharp library for resizing), video transcoding (FFmpeg), content moderation (ML classifiers), and feed fanout. The pipeline is horizontally scalable: each stage is a separate consumer group that can scale independently based on queue depth. Failed processing is retried 3 times with exponential backoff before moving to a dead-letter queue. The entire pipeline from upload completion to CDN availability targets under 30 seconds for photos and under 5 minutes for videos.
Q7: How does Instagram handle cache invalidation?
Instagram uses a multi-tier caching strategy with different invalidation approaches. For user profiles, a write-through pattern invalidates both L1 (in-process) and L2 (Redis) caches whenever the profile is updated. For feed caches, lazy invalidation with TTL is used: stale data is acceptable for a few seconds. For CDN media, versioned URLs eliminate the need for cache invalidation by treating edits as new resources. For like and comment counts, a write-through cache updates Redis synchronously with the database write, ensuring consistency within a single database transaction. Cache stampede prevention uses distributed locking via Redis SETNX with a double-check pattern to prevent thundering herd effects when popular content cache entries expire.
Q8: How would you scale Instagram to 5 billion users?
Scaling to 5 billion users requires several architectural changes. First, increase the fanout threshold from 500K to 2M followers to reduce read-time merge overhead. Second, implement geographic sharding where user data is partitioned by region, keeping data locality high. Third, add more Redis cluster nodes to accommodate the larger feed cache footprint (approximately 500 GB). Fourth, use edge computing for feed ranking to reduce latency for users far from data centers. Fifth, implement predictive pre-fetching where the client downloads the next page of the feed based on the user's scrolling patterns. Sixth, increase CDN edge locations and implement tiered caching with regional caches between the origin and edge. The key insight is that linear scaling of infrastructure is not sufficient; algorithmic optimizations like more aggressive feed pruning and model distillation for edge inference are essential at this scale.
Q9: How does Instagram detect and prevent spam?
Instagram uses a multi-layered spam detection system. At the API gateway level, rate limiting prevents automated posting beyond human-capable speeds (e.g., more than 100 likes per minute triggers a CAPTCHA). At the content level, ML classifiers analyze post content, caption text, and comment patterns to identify spam. At the behavioral level, the system detects anomalous patterns such as following/unfollowing thousands of accounts in a short period, posting identical content across multiple accounts, and coordinated engagement (groups of accounts liking each other's content in a fixed pattern). The spam score is computed in real-time and combined with the account's historical trust score. High-confidence spam is automatically removed; medium-confidence is queued for human review; low-confidence is logged for future model training.
Q10: What are the key trade-offs in the Instagram architecture?
The primary trade-offs are: (1) Consistency vs. Availability: Instagram prioritizes availability (99.99%) over strong consistency. Feed data and like counts are eventually consistent within 2-5 seconds, which is acceptable for social media but would not work for financial systems. (2) Latency vs. Cost: The three-tier cache reduces latency but increases infrastructure cost. Removing the L1 cache would save memory but increase Redis load by 40%. (3) Freshness vs. Efficiency: Fanout-on-write gives the freshest feeds but costs more in write amplification. The 500K follower threshold is the balance point. (4) Privacy vs. Features: Personalized recommendations require analyzing user behavior, which conflicts with strict privacy controls. Instagram addresses this with differential privacy techniques that add noise to aggregated user data used for model training. (5) Simplicity vs. Optimization: The hybrid fanout model is more complex than pure push or pure pull, but the performance benefits justify the added complexity at Instagram's scale.
Frequently Asked Questions
How does Instagram store billions of photos at scale?
Photos are stored in S3 with CloudFront CDN. Each upload generates four size variants. Metadata is stored in Cassandra and PostgreSQL. S3 lifecycle policies manage deletion and archival across five storage tiers from hot CDN edge to deep archive.
How does the Instagram feed algorithm work?
Instagram uses a hybrid fanout-on-write and fanout-on-read approach. For regular users, posts are pushed into followers' feed caches in Redis sorted sets. For celebrity users, posts are merged at read time. The feed is then ranked by a LightGBM model that predicts engagement probability.
How does Instagram Explore generate recommendations?
Explore uses collaborative filtering and content-based filtering to generate candidates. A LightGBM ranking model scores candidates by predicted engagement. Re-ranking applies diversity constraints. The pipeline runs in under 200ms using pre-computed features from a feature store.
How do Instagram Stories expire after 24 hours?
Stories use three expiration layers: S3 lifecycle policies delete the files, Redis TTLs expire the metadata, and the application checks timestamps at read time. Story Highlights bypass this by copying media to permanent storage when a user saves a story.
How does Instagram handle real-time direct messages?
DMs use MQTT over WebSocket for real-time delivery. Messages are stored in Cassandra for durability. Online users receive messages via WebSocket; offline users get push notifications. Read receipts and typing indicators use ephemeral Redis keys with 5-second TTLs.
What database does Instagram use for its primary data store?
Instagram uses PostgreSQL for relational data (user accounts, follows, likes, comments) and Cassandra for high-write-throughput data (media metadata, feed timelines, messages, story views). Redis handles caching and ephemeral state, and Elasticsearch powers search functionality.
How does Instagram handle content moderation at scale?
Every upload passes through automated ML classifiers that detect NSFW content, spam, copyright violations, and text toxicity. High-confidence violations are auto-removed. Medium-confidence content enters a human review queue with a 4-hour SLA. User reports are processed through a separate priority-scored pipeline.
What caching strategy does Instagram use?
Instagram uses a three-tier cache: L1 in-process LRU (5-second TTL, 40% hit rate), L2 Redis cluster (2-5ms latency, 85% hit rate), and L3 CloudFront CDN (10-30ms for media, 90% hit rate). Cache stampede prevention uses distributed locking with Redis SETNX.
Originally published on Ayodhyyya. Last updated July 1, 2026.