Nearby friends
0-day streakReview

Your progress

Level 1

0 XP100 XP to level 2
0day streak
0best streak
0topics started
0cards reviewed
    Review cards

    Progress is stored only in this browser. No account is required.

    Layer 4 · Full breakdown · 34 min · 13 figures

    Design Nearby Friends

    Design an opt-in nearby friends feature for a large social app: per-user location channels instead of a geo index, sharded Redis pub/sub, millions of WebSocket connections, battery-aware updates, and privacy defences against stalking and trilateration.

    Nearby Friends answers one small question for people who have chosen to share: which of my friends are close to me right now? Open the screen on a Saturday evening in Indiranagar and you see “Asha · less than 2 km · 1 min ago” and “Kabir · about 6 km · 12 min ago”, and the list shifts as people move. Facebook launched a feature like this in 2014; Snap Map and the live location sharing in Find My and Google Maps are close relatives. Three things make it a good design problem. Both ends move, so no answer stays true for long. The candidates are not everyone on the map but the people on your friend list, which changes the core data structure. And the data is dangerous: a location shown to the wrong person is not a privacy blip — it can put someone in physical danger. Here you design it for a large social app, from who may see whom to batteries and determined attackers.

    Understand the problem

    Start by naming what is unusual. In nearby places search the targets sit still and you ask the whole map “what is near me?”. In Uber drivers move, but riders query an index of every driver around a pickup. Here both sides move, and the friend graph fixes the candidates: the thirty or so friends who share with you, never a stranger two streets away. That difference drives most of the design.

    Functional requirements

    1. Users should be able to opt in and choose who can see them: all friends, close friends or a custom list, and pause or stop at any time. Sharing is mutual: you see a friend only if both of you share with each other.
    2. Users should be able to see which friends are nearby, within about 10 km, with an approximate distance and how long ago each location was updated. Friends farther away appear with only their city.
    3. The list should update live while the app is open. A friend who walks into range appears within seconds; one who leaves disappears.
    4. Users should be able to share a precise location with chosen friends for a limited time, say an hour, to find each other outside a crowded metro exit.

    Non-functional requirements

    • Scale. 120M opted-in users. At the evening peak about 7.2M have the app open, and the fleet receives about 250,000 location updates a second (derived below).
    • Freshness. While a friend has the app open, their position on your screen is at most about a minute old. An update reaches watching friends within 2 s at p99 of arriving at our servers.
    • Privacy as correctness. No one sees a location they were not given. Pause, remove and block take effect within seconds on every open screen. Coarse distances never pin anyone down more finely than about 1 km. The latest location expires after two hours; history is off unless the user turns it on.
    • Lossy positions, lossless permissions. A dropped location update is harmless — the next arrives within a minute. A dropped permission change is a privacy incident.
    • Availability. 99.9% for the live list. When something fails, show a slightly stale list rather than an error.
    • Battery. Never hold GPS on in the background, stay inside Android and iOS background limits, and send less when the phone isn’t moving.

    In the interview

    Ask early: “Is sharing mutual?”, “What radius counts as nearby?”, “Seconds or minutes of freshness?”, “Exact or approximate distance?” and “Do we also show strangers nearby?” The last answer changes the core data structure. Friends only means the friend graph picks the candidates; strangers means you need a geo index — and much stricter privacy.

    Out of scope

    • Nearby strangers. Discovering people you don’t know needs a different index; it is a follow-up below and the core of Design Tinder.
    • Map tiles and routing. The map under the pins is Design Google Maps.
    • The friend graph itself. Friendships, close-friend lists and blocks already exist in the social graph; we only read them.
    • Proximity notifications (“Kabir is nearby”) — a follow-up.
    • Activity tracks and timelines. We keep optional, short-lived history only; long-term tracks belong to Design Strava.

    The set-up

    Plan the approach

    Build each requirement simply — settings, a list built on demand, live updates, precise sharing — then spend the deep dives where this problem is won: geo index versus friend graph, fan-out, stateful connections, battery, safety and travel. Say the shape early: “every user has a channel; friends’ servers subscribe to each other’s channels; the subscriber’s server does the maths.”

    Core entities

    • User and friendship, from the existing social graph, including blocks and friend lists.
    • Sharing settings. Enabled, audience (all friends, close friends, a list, or all friends except some), paused, and a version number that goes up on every change.
    • Share edge. A derived, mutual pair: “Rohan may see Asha”. Precomputed per viewer, with each sharer’s settings version.
    • Location. User, latitude, longitude, accuracy in metres, timestamp, a 1 km grid cell and a moving flag. Only the latest per user is kept live.
    • Precise share. Sharer, viewers and an expiry time.
    • Subscription. Which server is listening to which user’s channel. Ephemeral and never stored durably.

    In the cache they look like this:

    loc:{asha}    { lat: 12.97194, lng: 77.64125, acc_m: 18, ts: 1793125412,
                    cell: "g1km:1441:8226", moving: true }            TTL 7200 s
    ver:{asha}    8                                                   settings version
    edges:rohan   [ asha@8, kabir@3, meera@12, … ]                    ~30 sharers
    psh:{asha}    { viewers: [rohan], expires_at: 1793129012 }        TTL = time left
    
    {asha} is a Redis Cluster hash tag: one user's keys share a slot.

    API

    Two transports: a WebSocket for everything live while the app is open, and ordinary HTTPS for settings and for background updates.

    While the app is open, the phone sends its location over the socket:

    { "type": "loc", "lat": 12.97194, "lng": 77.64125, "acc_m": 18, "ts": 1793125412, "moving": true }

    The server answers once with a snapshot, then sends only changes:

    { "type": "snapshot", "friends": [
      { "id": "asha",  "dist": "lt_2km", "updated_at": 1793125412 },
      { "id": "kabir", "dist": "km_6",   "updated_at": 1793124700 },
      { "id": "meera", "dist": "far", "city": "Mumbai", "updated_at": 1793121100 } ] }
    { "type": "update", "id": "kabir", "dist": "km_4", "updated_at": 1793125440 }
    { "type": "remove", "id": "asha" }

    Notice what is missing: coordinates. For ordinary sharing the phone receives a distance bucket, never a friend’s latitude and longitude; the privacy deep dive explains why.

    Settings and precise sharing:

    PUT /v1/nearby/settings
    { "enabled": true, "audience": { "type": "list", "list_id": "close_friends" } }
    
    200 OK
    { "version": 8 }
    POST /v1/nearby/precise-shares
    { "viewer_ids": ["rohan"], "duration_s": 3600 }
    
    201 Created
    { "share_id": "ps_31c", "expires_at": "2026-10-27T19:00:00+05:30" }
    POST   /v1/nearby/pause                  stop sharing now; optional "until"
    DELETE /v1/nearby/precise-shares/ps_31c  end a precise share early
    POST   /v1/nearby/locations              a batch of fixes from a background OS callback

    Back-of-envelope estimates

    These numbers decide whether the hard part is storage, writes or fan-out. (For the method, see back-of-envelope estimation.)

    Who is online. 120M opted-in users. At the 9 pm peak, assume 6% have the app open: 120M × 6% = 7.2M open connections. Another 40% allow background location with sharing on: 48M background sharers.

    Location writes. Open apps adapt to motion: a quarter are moving and send every 15 s, the rest are still and send every 60 s. 7.2M × (0.25 ÷ 15 + 0.75 ÷ 60) = 7.2M × 0.029 ≈ 210,000 updates/s. Background phones get a few fixes an hour from the OS; at 3 an hour, 48M × 3 ÷ 3,600 = 40,000 updates/s. Together, about 250,000 updates/s. A fixed 15-second timer for every open app would be 7.2M ÷ 15 = 480,000/s, so adapting to motion halves the load before any server-side cleverness.

    Friends who share. A 2011 study of the whole Facebook graph measured an average of about 190 friends and a median of 99. Plan for 250. If 12% of a user’s friends share with them, that is 30 sharing friends — the number that sizes everything per viewer in this design.

    Fan-out. An update goes only to sharing friends who have the app open right now. The baseline is 6%, but friends tend to be online at the same hours, so assume double: 30 × 12% = 3.6 watching friends per update. 250,000 × 3.6 = 900,000 deliveries/s from pub/sub to WebSocket servers. Because distances are shown as buckets, most deliveries change nothing on screen; if one in five does, phones receive about 180,000 pushes/s.

    Connection churn. If the app stays open for about 5 minutes, 7.2M ÷ 300 s = 24,000 connects/s. Each needs a snapshot — one batched read of 30 locations, so 24,000 × 30 = 720,000 key reads/s — and 30 subscriptions: 720,000 subscribes/s, plus as many unsubscribes. Subscription churn is in the same league as the message traffic; keep that number for the deep dives.

    Memory. A latest-location entry is about 150 bytes with its key, so even if all 120M users had a fresh one, 120M × 150 B = 18 GB. Live subscriptions: at most 7.2M × 30 = 216M server-channel pairs; at roughly 100 bytes each, about 22 GB across the pub/sub cluster.

    Servers. At a conservative 50,000 connections per WebSocket server, 7.2M ÷ 50,000 = 144, so about 200 servers with headroom for drains and a lost zone. Pub/sub sizing comes in the deep dive.

    Tip

    Fan-out is updates per second × watching friends per update — and “watching” means mutual, sharing and online right now, not “all friends”. Multiply by all 250 friends instead of 3.6 and you overestimate by about 70×.

    High-level design

    Build the system one requirement at a time; by the end it works for one region at moderate traffic.

    Users should be able to opt in and choose who can see them

    Settings are ordinary, low-traffic CRUD: a Settings API writes a user’s choices to a Settings DB and bumps their version. The interesting part is what happens next. Whether Rohan may see Asha depends on four facts: they are friends, neither has blocked the other, Asha’s audience includes Rohan, and Rohan’s audience includes Asha — sharing goes both ways, as Facebook’s original feature required. Checking all four on every location update would put the social graph on the hot path.

    Instead, an edge builder listens for changes to settings, friendships and blocks, and recomputes the affected share edges: for each viewer, the list of sharers they may see, each stamped with the sharer’s current version. The result lives in a cache that is read once when a phone connects. Changes are rare next to location updates, so the edges can be recomputed generously.

    Step 1: sharing settings are written once and turned into a list of mutual share edges per viewer.Notice The expensive question — “who may see whom?” — is answered when settings change, not on every location update.

    Users should be able to see which friends are nearby

    Start with the simplest thing that works: a list built on request. The phone sends its location to a stateless location service, which validates it and writes it to a location cache — Redis, one key per user — with a two-hour TTL. Only the latest position is kept, and the TTL means a phone that dies in a tunnel quietly drops off everyone’s list instead of showing a position from yesterday.

    When Rohan opens the list, a nearby service reads edges:rohan (30 IDs), fetches all 30 locations in one MGET, computes each distance with the haversine formula and returns friends within 10 km as distance buckets, sorted by bucket and then by name. Friends beyond 10 km show only a city; entries older than 30 minutes are greyed out.

    Step 2: phones write their latest location to a cache with a TTL; opening the list reads every sharing friend’s entry in one batch.Notice The list is built from about 30 keys chosen by the friend graph — no part of the map is ever scanned.

    This works, but keeping it live by polling is wasteful. If 7.2M open apps polled every 15 s, that is 480,000 requests and 14.4M key reads a second, and almost every response repeats what the phone already shows. Freshness also suffers at both ends: a friend’s update that is 15 seconds old, picked up by a 15-second poll, can be 30 seconds stale.

    The list should update live while the app is open

    Flip from pull to push (see push vs pull). Each open app holds one WebSocket to a WebSocket server and sends its location over the same socket, so the work of the location and nearby services moves into these servers. A WebSocket fits better than server-sent events because traffic flows both ways; see real-time connections.

    The key idea is a channel per user. Every location update from Asha is published once, on channel loc:asha, in a publish/subscribe system. When Rohan connects, his WebSocket server subscribes to the channels of his 30 sharing friends. When Asha moves:

    1. Her WebSocket server validates the fix, snaps it to its grid cell, writes it to the cache and publishes it on channel loc:asha.
    2. The pub/sub system delivers it only to servers that subscribed to that channel — here, the one holding Rohan’s connection.
    3. That server already knows where Rohan is, because his phone reports to it. It computes the distance in memory and, if the bucket changed, pushes one small message to Rohan.
    Step 3: one location update, published once on Asha’s channel and delivered only to the server holding her friend’s connection.Notice Where the distance is computed: on Rohan’s server, which already holds Rohan’s position, so the update needs no database read.

    Two choices deserve saying out loud. First, the subscriber’s server computes the distance, not the publisher’s. The publisher’s server would need the positions of all 30 of Asha’s friends — 30 reads per update, 7.5M reads a second — while the subscriber’s server already holds the one position it needs. Second, both sides moving costs nothing extra: when Rohan moves, his server recomputes his distance to all 30 friends from positions it already holds in memory, again with no reads.

    The server pushes only when something visible changes: a bucket, a friend entering or leaving 10 km, or a timestamp more than a minute newer than the one last sent. Most updates change nothing on screen, which is how 900,000 deliveries a second become about 180,000 pushes.

    Users should be able to share a precise location for a limited time

    A precise share is a row — sharer, viewers, expires_at — next to the settings, mirrored to the cache with a TTL equal to its remaining lifetime. While a share is active, Asha’s server also publishes her unsnapped position on a second channel, precise:asha. Only viewers named in the share subscribe to it, and only their phones receive coordinates, to draw a pin.

    Expiry is enforced at the source. Before every publish to precise:asha, her server checks that the share still exists and has not expired. When it lapses at 19:00 the precise channel simply goes quiet, and Rohan’s screen falls back to “less than 2 km”. An “expired” message to viewers is a courtesy, not the mechanism: if it is lost, nothing new leaks.

    Step 4: the working design. Settings become share edges, open apps hold sockets, every user has a channel, and precise sharing gets a channel of its own.Notice Expiry is enforced where a location enters the system, so a viewer that misses the “expired” message still receives nothing new.

    That is a working design: settings become share edges, phones hold sockets, every user has a channel, and subscribers compute distances. The deep dives push it to 250,000 updates a second and make it safe.

    Deep dives

    How do we find nearby friends: geo index or friend graph?

    The instinct from other proximity search problems is to build a geospatial index. Do the arithmetic before reaching for one.

    Bad: a geo index, then intersect with friends. Index every sharer’s latest location by cell — geohash, S2 or H3, as in geospatial indexes — and answer Rohan with “everyone within 10 km”, then keep only his friends. Suppose 1.2M sharers live in Bengaluru, 40% of them with a fresh location, spread over about 750 km². A 10 km circle covers π × 10² ≈ 314 km², about 42% of the city: 1.2M × 0.4 × 0.42 ≈ 200,000 candidates, to keep the 30 or fewer who are friends. You scan about 6,700 locations per sharing friend, and the index must also absorb 250,000 moves a second. Live updates are worse still: to notice a friend entering your circle, you would re-run that query every few seconds for 7.2M viewers.

    Good: let the friend graph pick the candidates. Rohan has 30 sharing friends: read their 30 latest locations and compute 30 distances. The cost per viewer is set by the number of friends, not by how crowded the city is, so it is the same in a village and in Koramangala.

    Great: friend-graph fan-out for friends, a geo index only where the product needs one. Push turns “30 reads per refresh” into none: the channel carries each friend’s update to the one server that needs it. Keep a geo index for features whose candidates really are strangers — “people nearby”, events — where no graph narrows the search. The crossover comes only where fresh locations inside the radius are fewer than the sharing friends, such as a sparse district and a viewer with hundreds of sharing friends; even there the friend graph stays cheap, so one design for everyone is simpler.

    Two ways to answer “which of Rohan’s friends are within 10 km?” in Bengaluru at 9 pm.Notice The asymmetry: the geo index touches every sharer in the circle to find a handful of friends; the friend graph touches only the friends.
    Geo index, then intersect Friend-graph channels
    Candidates per look Everyone inside the radius (≈ 200,000 in a big city) Sharing friends (≈ 30)
    Cost grows with Local density Friend count, capped by the 6,000-friend limit
    Live updates Re-query on a timer Pushed on every friend’s move
    Write path Every move updates a spatial index One cache write and one publish
    Strangers (“people nearby”) Natural fit Not possible
    Privacy surface Index holds everyone around you Only mutual sharers are ever read
    Two ways to find nearby friends

    How do we fan out 250,000 updates a second?

    Every update must reach the few servers holding that user’s watching friends. Per-user channels mean up to 120M channels, almost all of them with zero to a handful of subscribers.

    Bad: a log such as Kafka. A topic per user is out of the question at 120M users, so you would use one Kafka topic partitioned by user ID. But a WebSocket server can’t know in advance which partitions its viewers’ friends land in, so in practice every server reads everything: 250,000 messages a second × 200 servers = 50M reads a second, of which 900,000 are useful — 98% is thrown away. Kafka belongs on the history path, not the routing path.

    Good: Redis pub/sub. Channels in Redis cost nothing until someone subscribes, a publish is pushed straight to the subscribed connections, and the guarantee is what positions need: at-most-once. A message that finds no subscriber is simply gone — fine for a location replaced within a minute (see delivery guarantees). The catch is scaling. In a Redis Cluster, a classic PUBLISH propagates to every node, so every node handles every message: adding nodes adds copies, not capacity.

    Great: sharded pub/sub. Since Redis 7.0, SPUBLISH and SSUBSCRIBE assign each channel to a hash slot with the same algorithm used for keys, and messages stay inside the shard that owns the slot. Now loc:asha lives on one shard, its subscribers connect to that shard’s primary or one of its replicas, and capacity grows with every shard you add.

    One location update in a Redis Cluster: classic PUBLISH (left) versus sharded SPUBLISH (right).Notice Classic pub/sub copies every message to every node, so more nodes mean more work; sharded pub/sub keeps each message inside the shard that owns its channel.

    Size the cluster on deliveries, not on channels. The peak is 250,000 publishes and 900,000 deliveries a second, and a big night such as New Year’s Eve can triple it. Take a planning budget of about 150,000 deliveries a second per primary, which leaves its main thread headroom: 900,000 × 3 ÷ 150,000 = 18, so about 20 primaries, each with a replica. On a normal peak each primary delivers 45,000 messages a second and holds about 11M subscriptions (216M ÷ 20), roughly 1.1 GB.

    Three details keep it efficient:

    • Subscribe once per server, not once per user. If two of a server’s users want Asha, the server subscribes once and fans out in memory.
    • Make churn cheap. 720,000 subscribes a second rival the message traffic. Pipeline them per shard, and unsubscribe lazily — keep a channel for two minutes after its last local listener leaves, because people reopen apps.
    • Keep pub/sub connections separate. Under the older RESP2 protocol a subscribed connection can’t run ordinary commands, so use a few dedicated pub/sub connections per shard per server — never one per user.

    Resizing is the painful part. Subscriptions live on connections, not in the keyspace, so they can’t be copied to a new shard. When slots move, every server subscribed to a moved channel must connect to the new owner and subscribe again, and whatever is published in between is lost; a primary failing over to its replica opens the same gap. You can’t avoid it, but you can make it harmless: after resubscribing, the server re-reads those friends’ latest locations from the cache, so the screen is right at once. Move a few slots at a time, in quiet hours, with jittered resubscribes. The same holds for standalone Redis nodes on a consistent hashing ring: adding one node to a ring of N moves about 1/(N + 1) of the channels.

    What happens to one subscription when the slot holding loc:asha moves to a new shard.Notice The loss is bounded: one update falls into the gap, and a single cache read means Rohan’s screen never shows it.

    In the interview

    Say the guarantee out loud: “I publish each update once, on the user’s own channel, and let pub/sub find the three or four servers that care. Redis pub/sub is at-most-once, and that is the right guarantee for positions, because the next one is 15 to 60 seconds away. It would be the wrong guarantee for permission changes, so those don’t depend on it.”

    How do we run 7 million stateful connections?

    The WebSocket servers are the only stateful tier users touch, holding 7.2M connections at peak. Idle sockets are cheap on an event-driven server — Design WhatsApp shows one machine holding millions — but each connection here carries real work: a snapshot, 30 subscriptions and continuous distance updates. Sizing at 50,000 per server also keeps a crash small: 50,000 reconnects, not a million.

    Bad: make the socket server the source of truth. If a friend’s latest position or a user’s settings live only in the memory of whichever server holds a connection, a crash or deploy loses them, and users can’t move between servers without their state.

    Good: keep the server’s state disposable. Everything a server holds — the viewer’s position, the 30 friends’ latest positions and versions, the buckets last sent to the phone — can be rebuilt from the caches with one edges read, one MGET and 30 subscriptions. A reconnect to any server is therefore a fresh snapshot, never a resume problem; see stateful vs stateless design. Mobile networks drop idle connections silently, so send WebSocket Ping frames — RFC 6455 explicitly allows them as keepalives — every 30 seconds or so, and close connections that miss two Pongs (more in heartbeats).

    Great: plan for mass reconnects. The dangerous moments are deploys and crashes, when tens of thousands of phones reconnect at once, and every reconnect costs a TLS handshake, an edges read, a 30-key read and 30 subscribes.

    • Drain on deploy. Take the server out of the load balancer, then close its connections in small batches over about 60 seconds with close code 1001, which RFC 6455 defines as “going away”. 50,000 connections become about 830 reconnects a second.
    • Jitter on the client. RFC 6455 tells clients to wait a random delay before reconnecting after an abnormal closure — it suggests 0 to 5 seconds — and to back off exponentially if that fails. A crash then becomes five seconds at 10,000 reconnects a second instead of one spike of 50,000; it is the same idea as backoff with jitter.
    • Admission control. A server already accepting reconnects faster than its budget rejects new ones with a retry hint, instead of accepting everything and timing out — the cheapest defence against a retry storm.
    Reconnects per second when a WebSocket server holding 50,000 connections goes away, three ways.Notice The spread: the same 50,000 reconnects arrive at 50,000 a second as a spike, about 10,000 a second with a random 0–5 s delay, and about 830 a second with a 60-second drain.

    The phone needs a fallback too. If it can’t hold a socket — a captive Wi-Fi portal, a proxy that refuses the upgrade — it polls the snapshot over HTTPS every 60 seconds. The list is less live but still correct.

    How do we keep locations fresh without draining the battery?

    The phone is half of this system. A design that asks for GPS every few seconds forever will be throttled by the operating system and uninstalled by the user, and both mobile platforms limit background location hard.

    Bad: a fixed, fast timer. One GPS fix every 10 seconds, whether the user is walking or asleep. In the background it doesn’t even work: since Android 8.0, background apps receive location updates only a few times each hour, however often they ask.

    Good: foreground only, on a fixed interval. Send every 30 seconds while the app is open and nothing otherwise. Simple and kind to the battery — but friends then see only the people who happen to have the app open, a small fraction at any moment.

    Great: adaptive sampling that uses the OS’s cheap signals.

    • Motion-aware intervals in the foreground. Use the platform’s activity recognition: every 15 seconds while walking or in a vehicle, every 60 seconds when still. Add a distance filter that skips updates of under 50 m unless a minute has passed, so “last updated” stays honest.
    • Let the OS wake you in the background. On iOS, the significant-change location service delivers an update after the phone moves about 500 metres, no more often than every five minutes, and can even relaunch a terminated app to deliver it. On Android, a geofence around the current position wakes the app when the user leaves it; geofence events reach background apps more often than plain location updates, typically within a couple of minutes. A foreground service with a visible notification can do more, but keep it for explicit live sharing — and on Android 11 and later, one started from the background needs the background location permission.
    • Ask only for the accuracy you need. “About 6 km” needs a coarse, network-based fix, not GPS; switch GPS on only during a precise share, and treat a user’s “approximate location only” grant as coarse.
    • Batch and timestamp. Fixes collected offline go up together, each with its own timestamp; the server keeps the newest.
    One evening of location updates from Priya’s phone with adaptive sampling.Notice Two-thirds of the updates fall in the 25 minutes she is moving with the app open; at home in the background, the OS allows only a few.

    Priya’s evening shows the effect: 150 updates in four hours instead of 960, and most of the saving comes while nothing is happening. The server also keeps locations honest: it rejects fixes with poor accuracy (worse than 2 km), timestamps in the future, and impossible jumps — a phone that “moves” 400 km in a minute is a bug or a spoofing tool. Those checks matter again in the next deep dive.

    How do we protect privacy and keep people safe?

    For most features, privacy is a policy layer. For this one it is the design. A location leak can help an abusive ex-partner find someone who has moved away, or let a stalker learn a daily routine. Treat each requirement here as a correctness property, and test it like one.

    Consent and control. Follow the patterns the original products settled on:

    • Off by default, mutual, with chosen audiences. Nothing is shared until a user turns the feature on and picks an audience — friends, close friends or a list — and two people see each other only if both share. Snap Map’s published rules are similar: only friends who have added each other back.
    • Pause without a signal. Pausing makes you look exactly like someone whose phone is off, with no “paused” label, as with Snap Map’s Ghost Mode. A person hiding from someone shouldn’t have to announce it.
    • Expiring precision. Exact locations go only to chosen people and only for a set time, such as an hour, as in Facebook’s original feature. Open-ended sharing should be a separate, deliberate choice with periodic reminders that it is on — Snap Map nudges users who leave sharing on for a while.
    • Fade out. Latest locations expire after two hours; Snap Map hides people who haven’t opened the app for 24 hours.

    Distances that can’t be turned into coordinates. Now assume the attacker is a “friend” — a fake profile someone accepted, or an ex still on the list — running a modified app.

    Bad: send friends’ coordinates to the phone and compute distance there. It is the easiest implementation, and it leaks everything: anyone who reads the app’s network traffic sees exact positions.

    Good: compute on the server and show a rounded distance. Better, but not enough. If an attacker can pretend to be in different places, they measure the distance from three points and intersect the circles — trilateration. Rounding barely helps, because the attacker moves until the rounded value flips and so learns where the boundary lies. In 2015, researchers at Columbia University located Facebook Nearby Friends users to within 5 metres in about 3 seconds, using a single account, even though distances were rounded to at least half a mile. One weakness they used: the location sent with a “who is nearby?” query was not tied to the account’s last reported location, so the speed checks on location updates didn’t limit queries.

    Great: spatial cloaking, computed only from verified locations.

    • Snap before you compute. Map both the viewer’s and the friend’s location to the centre of a fixed grid cell 1 km across before any distance is calculated. The best an attacker can learn is the cell; the same paper shows the expected distance from a cell’s centre to the true position is about 0.38 times the cell size — about 380 m here. The price is accuracy: a displayed distance can be off by up to √2 × 1 km ≈ 1.4 km, which is why the smallest bucket says “less than 2 km”, not “0.4 km away”. Facebook and Foursquare both adopted this kind of cloaking after the researchers reported their findings.
    • One location per user. Distances are always computed from the viewer’s own last reported, plausibility-checked location, never from coordinates sent with a request. That takes away the attacker’s free choice of vantage point.
    • Limit and coarsen. Show only a city beyond 10 km — large disclosed distances let an attacker shrink the search area fast. Recompute a pair’s bucket at most once a minute, and flag accounts whose location jumps implausibly (see rate limiting). Query limits alone only slow an attack — to about 100 minutes against Facebook at one query per 5 minutes, the researchers estimated — so they are a second layer, not the defence.
    • Don’t leak through order or timing. Sort by bucket, then by name: one app the researchers studied hid distances but still sorted people by distance, and that was enough. Publish cells on the regular schedule, with a little hysteresis at borders, so the moment someone crosses into a new cell isn’t visible.
    Left: three exact distances pin Asha down. Right: with spatial cloaking on a 1 km grid, the best an attacker can learn is her cell.Notice Rounding the displayed number doesn’t stop the attack on the left — the attacker just moves and measures again — but snapping the location before any distance is computed does.

    Revocations that can’t get lost. When Asha removes Rohan from her audience, the obvious move is to send a “revoke” message on her channel so that Rohan’s server drops her. But pub/sub is at-most-once: if that message is lost, Rohan’s server keeps receiving her updates. So the guarantee has to come from the durable side:

    1. The Settings API commits the change, rebuilds the affected share edges, and moves Asha’s version from 7 to 8.
    2. Every location Asha publishes carries her current version, read in the same round trip as the cache write; her location and version keys share a hash tag, so they live in one slot.
    3. Rohan’s server remembers the version at which it last confirmed each friend. A message with a newer version triggers a re-check of edges:rohan before anything is shown. If Asha is no longer there, the server drops the message, removes her from Rohan’s screen and unsubscribes.
    4. Every server re-checks all its connections’ friend lists every 5 minutes, which catches friends who stop sending updates.

    The fast “revoke” message is still sent — it usually makes Asha disappear within a second — but correctness no longer depends on it.

    Asha removes Rohan while he is watching. The “revoke” message is lost, but the version on her next update stops the leak.Notice The guarantee doesn’t depend on the at-most-once channel: the durable settings write and the version check do the work.

    Lossy is fine for positions, not for permissions

    Never let a privacy guarantee depend on a fire-and-forget channel. A lost position is replaced within a minute; a lost “stop sharing with Rohan” is a leak. Make the durable write the source of truth, and check it where the data is delivered.

    Blocking, retention and coercion. A block removes share edges in both directions at once, through the same versioned path. Keep only what the feature needs — the latest location for two hours, no history unless asked for — and make deletion reach caches, history and backups on a published schedule. A reminder listing everyone who can see you (“You’re sharing with 12 people”) and a one-tap pause help someone who was pressured into sharing.

    Aggregates leak too

    In January 2018, analysts noticed that Strava’s public heatmap of users’ recorded activity revealed military bases and even their internal layout in places such as Afghanistan and Syria. Location data is sensitive in aggregate as well as one person at a time, so never reuse Nearby Friends data for heatmaps or “popular places” without strict thresholds. Design Strava covers that story in depth.

    How do we store location history, if at all?

    The live feature needs only the latest position. History — “where was I last Sunday?” — is a separate, opt-in product, and the default answer is not to keep it.

    Bad: write every update to the main database. 250,000 durable writes a second at peak, for data the feature never reads — and an attractive target for attackers and subpoenas.

    Good: latest only. The location cache with its two-hour TTL is the whole store: cheap, private by construction, and self-healing after a node loss as phones send their next updates.

    Great: opt-in history on a separate, expiring path. For users who turn history on, the WebSocket servers append updates to a stream such as Kafka, and a writer stores them in a wide-column store like Cassandra, partitioned by user and day, sorted by time, with a 30-day TTL on every row. If 20% of sharers enable history and the daily average is 40% of the peak rate, that is 250,000 × 0.4 × 0.2 = 20,000 points a second, or 1.7 billion a day; at about 40 bytes each, roughly 70 GB a day, 2.1 TB for 30 days, and about 6.2 TB with three replicas. Deleting your history drops whole partitions; turning it off stops the stream at the source.

    How do we handle travel, regions and hot users?

    Regions. Run the stack in several regions — say Mumbai for South Asia, Singapore for South-East Asia and Frankfurt for Europe. The usual worry in multi-region designs is that related users end up in different places. This problem comes with a gift: “nearby” means physically close, and a phone connects to the region nearest to where it is now. Friends who are actually near each other are almost always in the same region, so their channels, caches and subscriptions are local, and any pair split across regions is far apart by definition.

    • Route by geography, not by measured latency, so everyone in Bengaluru lands in the same region even when a network path briefly prefers Singapore.
    • Replicate only coarse data globally: each user’s current city, updated at most every 10 minutes. That is all a faraway friend sees (“Priya · in Singapore”), much as Facebook’s original feature showed a travelling friend’s city.
    • Settings and share edges change slowly, so they replicate asynchronously from a home region.
    • Friends near a region boundary are rare enough for an exception path that bridges their channels.
    Phones connect to the region nearest to where they are now, so friends who are actually near each other share a region.Notice Only a coarse city crosses between regions; every nearby and precise update stays inside one region.

    When Priya flies from Bengaluru to Singapore, nothing has to move explicitly. After landing her phone reconnects in Singapore, her location is written to the Singapore cache, and Wei, who lives there, sees her “less than 2 km” away. Rohan, back home, sees only the city.

    Hot users. Followers never see locations — only friends do — and Facebook caps friends at 6,000, so the usual celebrity problem can’t arise. Even a user at the cap, with 12% of friends sharing and 12% of those online, has about 86 watching friends per update against 3.6 on average: bigger, but trivial for one shard. The other direction matters more. The friendship paradox — your friends tend to have more friends than you — means subscribers skew towards large friend lists, and a user with 6,000 friends opens the app with 720 subscriptions and a 720-key snapshot. Read it in batches, nearest cells first, and it is fine.

    Hot places. The real spikes are local: an IPL final, New Year’s Eve on Brigade Road, a music festival. Everyone is within 2 km of everyone and many people open the app at once. Bucketing helps: when a whole stadium of friends is “less than 2 km”, most deliveries change nothing on screen, and the pub/sub cluster was already sized for three times the normal peak.

    The final design

    Put the pieces together. The live path is phones, WebSocket servers, pub/sub and the location cache; settings and history sit to the side, and nothing on the live path waits for a database.

    The complete Nearby Friends design for one region, with numbered steps for one viewer and one sharer.Notice The only durable writes are settings and opt-in history; everything on the live path is in memory and can be rebuilt from the caches.

    Follow Rohan opening the app while Asha walks towards him:

    1. Connect. Rohan’s app opens a WebSocket through an L4 load balancer to one of about 200 WebSocket servers, which authenticates it and starts heartbeats.
    2. Load permissions. The server reads edges:rohan: the sharers Rohan may see, each with its settings version.
    3. Snapshot. One MGET fetches those friends’ latest cells from the location cache; the server computes buckets and sends the first list within a few hundred milliseconds.
    4. Subscribe and publish. The server subscribes to the 30 friends’ channels, once per channel per server. Meanwhile each of Asha’s location frames is checked for accuracy and plausible speed, snapped to its 1 km cell, written to the cache with a two-hour TTL alongside a read of her version, and published once on loc:asha — and on precise:asha while a precise share is active.
    5. Deliver and compute. The owning shard pushes Asha’s update to the three or four subscribed servers. Rohan’s server checks the version, computes the distance from his position, and pushes a change only if the bucket or the freshness moved.
    6. Background. Phones in the background post OS-delivered fixes over HTTPS to the same tier, which runs step 4 for them without a socket.
    7. Change settings. The Settings API writes versioned settings; the edge builder recomputes share edges; a newer version stops old permissions at the next update, and the 5-minute re-check catches the rest.
    8. History. Only for users who asked for it, updates are appended to the location stream and stored with a 30-day TTL.

    Check it against the requirements. About 250,000 updates a second become one cache write and one publish each, and 900,000 deliveries spread over 20 primaries. A moving friend reaches watching screens within the 2-second target, and a still one refreshes every minute. No coordinates reach a phone except through an expiring precise share, distances come only from cloaked, verified locations, and permissions are enforced by versions, not by a lossy message. All live state expires on its own or can be rebuilt from the caches.

    What interviewers look for by level

    Area Mid-level Senior Staff
    Requirements Friends on a map Mutual opt-in, radius, freshness and approximate distances agreed up front Privacy and safety framed as correctness requirements with their own targets
    Candidates Geo query for everyone nearby Friend graph picks the candidates, justified with numbers Knows when a geo index wins (strangers, events) and keeps both paths
    Fan-out Polling Per-user channels; the subscriber computes distance Sharded pub/sub, per-server dedupe, churn and resize costs
    Connections “Use WebSockets” Heartbeats, disposable server state, snapshot on reconnect Drains with close code 1001, jittered reconnects, admission control, polling fallback
    Mobile Fixed timer Foreground and background tiers Motion-aware sampling, significant-change and geofences, accuracy budgets
    Privacy A settings toggle Server-side distance, audiences, expiring precise shares Spatial cloaking against trilateration, versioned revocation, coercion and retention
    Scale and geography One region Shards the cache and pub/sub Regions by current location, coarse global data, bounded hot users

    Follow-up questions

    What if the pub/sub cluster goes down?

    Positions stop flowing, but nothing that matters is lost: the cache still holds every latest location. WebSocket servers notice the broken subscriptions and fall back to polling the cache for their users’ friends. Every 30 seconds that is 7.2M × 30 ÷ 30 = 7.2M key reads a second, ten times the cache’s normal read load, so poll still friends every few minutes and read from replicas. When pub/sub returns, servers resubscribe with jitter. Permissions never relied on pub/sub, so privacy is unaffected.

    What if we want to show nearby people who aren’t friends?

    Then the friend graph can’t pick candidates and you need a geo index: users bucketed by H3 or geohash cell in sharded in-memory sets, searched ring by ring around the viewer, as in Design Tinder. Privacy gets stricter, not looser: a separate opt-in, larger cells, no distance at all for strangers (just “nearby”), tight limits on how often a viewer’s location may change, and detection of accounts that probe from many places.

    What if users want a notification when a friend comes nearby?

    Detect it where the distance is already computed. For viewers with the app open, the WebSocket server sees the bucket change. For phones in the background, a small job consumes updates from users who enabled alerts, compares each with their sharing friends’ cached cells, and emits an event when a pair comes within 2 km. Add hysteresis — enter at 2 km, leave at 5 km — and a cool-down of about a day per pair, or people will mute it, and deliver through the notification system. Both people must have allowed alerts about each other.

    What if a group wants a shared live map for a day trip?

    Make the group the channel. A trip to Nandi Hills with eight friends creates group:g42; members publish precise locations to it until the trip’s end time, and each member’s server subscribes once. Fan-out per update is the group size, expiry is built in, and the same version checks apply when someone leaves the group.

    What if a user’s account is taken over?

    An attacker could add themselves to a precise share or quietly widen the audience. Require recent re-authentication before enabling precise sharing or adding anyone to a location audience, notify every device the user owns when the audience grows, and keep “who can see me” one tap away. A stolen session token alone should never be enough to change location settings silently.

    How to say it in the interview

    “I’ll design an opt-in nearby friends feature: 120 million users who share, about 7 million with the app open at the evening peak, and one rule above all — nobody ever sees a location they weren’t given.

    The key observation is that the candidates come from the friend graph, not the map. A geo index around a viewer in Bengaluru holds a couple of hundred thousand people to find the 30 or so friends who share with them, so I don’t build one. Instead every user has a pub/sub channel. A phone with the app open holds a WebSocket and sends its location every 15 to 60 seconds, depending on whether it’s moving; in the background the OS allows a few updates an hour. That’s about 250 thousand updates a second.

    Each update is written to Redis as the user’s latest location with a two-hour TTL and published once on their channel in a sharded Redis pub/sub cluster. When a viewer connects, their server reads their share edges, fetches 30 locations for the first screen, and subscribes to those 30 channels. When a friend moves, only the three or four servers watching get the message; each already knows its viewer’s position, computes the distance in memory, and pushes a bucket only if it changed. Pub/sub is at-most-once, which is fine for positions because the next one is seconds away.

    Operationally, socket servers hold only state they can rebuild from the caches: deploys drain over a minute, clients reconnect with jitter, and after a resize or failover the server re-reads the cache.

    Privacy is the hardest part. Sharing is mutual and audience-based, pausing looks like being offline, precise location goes only to chosen people for a fixed time, and history is off by default. Distances are computed on the server from each user’s own verified location, after snapping both people to a 1 kilometre grid, so trilateration can’t do better than a cell; the phone gets ‘less than 2 km’, never coordinates. And permission changes don’t rely on the lossy channel: every update carries the sharer’s settings version, and the receiving server re-checks before it shows anything.”

    Concepts used in this design

    References and further reading

    1. Introducing a new optional feature called Nearby Friends (Facebook Newsroom, 2014)about.fb.comThe original product: opt-in and two-way, audiences such as close friends or a list, precise location for a set time, and city-level travel.
    2. Snap Map: privacy by product (Snap Inc.)values.snap.comBidirectional friends only, Ghost Mode, live location as a separate choice, hiding people who haven’t opened the app for 24 hours, and reminders.
    3. Redis Pub/Sub documentationredis.ioAt-most-once delivery, channels independent of the keyspace, subscribed-mode limits, and sharded Pub/Sub (Redis 7.0) versus cluster-wide propagation.
    4. RFC 6455: The WebSocket Protocolrfc-editor.orgPing and Pong as keepalives, close code 1001 “going away”, and reconnecting after a random 0–5 s delay with exponential backoff.
    5. Background location limits (Android Developers)developer.android.comFrom Android 8.0, background apps get location only a few times each hour; foreground services, geofencing and batched updates as alternatives.
    6. startMonitoringSignificantLocationChanges() (Apple Core Location)developer.apple.comUpdates after moves of about 500 m, no more often than every five minutes, and relaunching a terminated app in the background.
    7. Where’s Wally? Precise User Discovery Attacks in Location Proximity Services (Polakis et al., ACM CCS 2015)cs.columbia.eduTrilateration located Facebook users within 5 m despite rounding; spatial cloaking, its privacy and error bounds, and why query limits only slow attackers.
    8. The Anatomy of the Facebook Social Graph (Ugander, Karrer, Backstrom and Marlow, 2011)arxiv.orgAn average of about 190 friends and a median of 99, the cap at the friend limit, and the friendship paradox.
    9. Why can’t I add someone as a friend? (Facebook Help Center)facebook.comThe current friend limit of 6,000, which bounds fan-out per user.
    10. Fitness tracking app Strava gives away location of secret US army bases (The Guardian, 2018)theguardian.comHow a heatmap of opted-in activity exposed military bases: location leaks in aggregate, not only one person at a time.