Hey everyone, Alex here from Coding with Alex. Welcome back to the blog.
If you hopped onto Hacker News today, you probably saw a captivating interactive project sitting near the top of the front page: a 3-D live satellite tracker rendering the real-time positions of over 30,000 satellites, space debris, and the massive Starlink constellation. It's an incredibly smooth, visually stunning piece of work. But as developers, once the initial "oooh, shiny" wears off, our engineering brains immediately pivot to the underlying architecture. We start asking: How does this scale? How do you calculate the orbital mechanics of 30,000 objects in real-time without melting the user's browser or racking up a massive cloud bill?
Today, we are going to dive under the hood of real-time spatial tracking. We will explore how to model orbital physics in code, how to leverage spatial indexing algorithms like Google's S2 and Uber's H3 to handle high-frequency geospatial queries, and how to build a highly performant, real-time spatial pipeline using Node.js, WebGL, and Redis. Grab your coffee; we are going to orbit.
The Physics Core: Simplifying Orbital Math with SGP4
To track a satellite, you cannot rely on simple linear math. Satellites do not travel in straight lines, nor do they follow perfect circles. They are locked in elliptical orbits influenced by Earth's non-uniform gravitational field, atmospheric drag, and solar radiation pressure.
Fortunately, the aerospace community solved the math decades ago. The standard model for predicting the position and velocity of near-Earth satellites is the Simplified General Perturbations 4 (SGP4) propagator. To run SGP4, you need a standardized data format known as a TLE (Two-Line Element Set). A TLE is a text format containing the orbital elements of an object at a specific point in time (known as the epoch).
Here is what a TLE looks like for a typical Starlink satellite:
STARLINK-30438
1 58345U 23174A 23318.51478148 .00014389 00000-0 10113-3 0 9997
2 58345 53.1583 23.8914 0001341 85.2219 274.9123 15.06403217 1235
To make this usable for web developers, we don't have to write the differential equations from scratch. There are excellent open-source implementations of SGP4 available. In the JavaScript/TypeScript ecosystem, the satellite.js library is the industry standard for translating TLEs into latitude, longitude, and altitude coordinates in real-time.
Calculating Real-Time Coordinates in Node.js
Let's look at how we can ingest a TLE and calculate a satellite's exact geodetic position for the current millisecond:
const satellite = require('satellite.js');
// Sample TLE for Starlink-30438
const tleLine1 = "1 58345U 23174A 23318.51478148 .00014389 00000-0 10113-3 0 9997";
const tleLine2 = "2 58345 53.1583 23.8914 0001341 85.2219 274.9123 15.06403217 1235";
function getSatellitePosition(tle1, tle2, date = new Date()) {
// Initialize the satellite record from TLE lines
const satrec = satellite.twoline2satrec(tle1, tle2);
// Propagate the orbit to the target date
const positionAndVelocity = satellite.propagate(satrec, date);
const positionEci = positionAndVelocity.position;
if (!positionEci) {
throw new Error("Could not propagate satellite position.");
}
// Convert ECI (Earth-Centered Inertial) coordinates to Geodetic (lat, lng, alt)
const gmst = satellite.gstime(date);
const positionGd = satellite.eciToGeodetic(positionEci, gmst);
// Convert radians to degrees, and altitude to kilometers
const longitude = satellite.degreesLong(positionGd.longitude);
const latitude = satellite.degreesLat(positionGd.latitude);
const altitude = positionGd.height; // in km
return { latitude, longitude, altitude };
}
console.log(getSatellitePosition(tleLine1, tleLine2));
// Output: { latitude: 34.1205, longitude: -118.4102, altitude: 542.13 }
The Scaling Bottleneck: Spatial Querying at Scale
Calculating the position of one satellite is trivial. But what happens when you have 30,000 satellites, and a user loads an interactive map zoomed in on western Europe? The user's client-side map only needs to display the satellites that are currently within their viewport bounding box.
If you perform a naive database scan running SGP4 on all 30,000 satellites, computing their coordinate, and checking if they fall within a bounding box, your CPU will cry. If you have 10,000 concurrent users panning across a map, your server infrastructure will collapse under the weight of $O(N)$ spatial lookups.
We need a way to index the sky. This is where discrete global grid systems (DGGS) come to the rescue.
Using Uber's H3 Hexagonal Hierarchical Spatial Index
To index space efficiently, we can project the Earth’s surface into a grid of cells. While traditional databases use R-Trees or B-Trees for spatial indexing, modern high-scale systems rely on index libraries like Google's S2 (based on square/quadtree projections) or Uber's H3 (based on hexagons).
Uber's H3 is particularly brilliant for spatial tracking because hexagons have an invariant distance between a cell center and all of its neighbors. This makes radial searches ("find all satellites within 500km of Paris") mathematically elegant and incredibly fast.
Instead of indexing by floating-point coordinates (latitude/longitude), we map every satellite to an H3 index (a 64-bit integer represented as a hexadecimal string) at a specific resolution.
Here is how you index a satellite's real-time position with H3 in Node.js:
const h3 = require("h3-js");
// Take our satellite coordinates
const lat = 34.1205;
const lng = -118.4102;
// Choose a resolution (Resolution 5 has an average cell area of ~250 square km)
const h3Resolution = 5;
const h3Index = h3.latLngToCell(lat, lng, h3Resolution);
console.log(h3Index); // Output: '852f012ffffffff'
An Elegant Architecture for Low-Latency Geospatial Queries
By mapping spatial coordinates to static index strings, we can completely bypass expensive geometry calculations on runtime queries. We can store this data in a fast, in-memory cache like Redis. Here is an architectural blueprint for how a production tracker runs:
- The Ingestion Worker: A background worker runs continuously. Every 1 to 5 seconds, it pulls the latest TLE dataset, calculates the current positions of all 30,000 satellites using SGP4, computes their H3 indices, and writes them to a Redis sorted set or hash.
- The Redis Store: We store satellites keyed by their current H3 cell. We can also use Redis Geohashes for native geospatial sorting.
- The Client API: When a client requests satellites for their current viewport, the API translates the viewport bounding box into a set of covering H3 hexagons, queries Redis for the pre-calculated satellites matching those hex IDs, and returns a lightweight JSON payload.
// Finding neighboring cells for a fast viewport look-up
const userLocationHex = h3.latLngToCell(48.8566, 2.3522, 5); // Paris
const viewportHexagons = h3.gridDisk(userLocationHex, 2); // K-ring of 2 hexes out
// Now we can pull only the satellites stored under these specific keys from Redis!
console.log(viewportHexagons);
// Output: [ '851f1d13fffffff', '851f1d17fffffff', ... ]
The Client-Side: Rendering 30k Objects with WebGL and Instanced Meshes
Once the backend serves up the coordinates, we face the final hurdle: rendering. If you attempt to render 30,000 satellites as individual DOM elements or standard Three.js mesh objects on a 3D globe, your frame rate will plummet to single digits. The browser's main thread will choke on draw calls.
To achieve a buttery-smooth 60 FPS, you must use Instanced Rendering via WebGL (often wrapped in libraries like Three.js or deck.gl).
What is Instanced Rendering?
In a standard rendering loop, if you want to draw 30,000 satellites, the CPU must send a separate instruction (a draw call) to the GPU for each satellite: "Draw satellite 1 at position X, draw satellite 2 at position Y...". The overhead of these CPU-to-GPU communications is the ultimate bottleneck.
With Instanced Meshes, you define the satellite geometry (a simple sphere or cross) once, and you pass an array of 30,000 transformation matrices (containing positions, scales, and rotations) to the GPU in a single draw call. The GPU then parallelizes the rendering of all 30,000 instances instantly.
Here is a simplified example of how you set up an instanced mesh for satellite positions in Three.js:
import * as THREE from 'three';
const count = 30000;
// Create a simple low-poly geometry for the satellite
const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
// Instantiate the InstancedMesh
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);
// Create a dummy object to handle spatial transformations
const dummy = new THREE.Object3D();
// Update positions (this loop runs inside your animation requestAnimationFrame)
function updateSatellites(satelliteDataList) {
satelliteDataList.forEach((sat, index) => {
// Convert Earth-Centered Coordinates to 3D Space Coordinates (x, y, z)
const { x, y, z } = convertLatLonToVector3(sat.lat, sat.lng, sat.alt);
dummy.position.set(x, y, z);
dummy.updateMatrix();
// Apply the transformation matrix to this specific instance
instancedMesh.setMatrixAt(index, dummy.matrix);
});
// Tell the GPU that the instance matrices have been updated
instancedMesh.instanceMatrix.needsUpdate = true;
}
Wrap-Up: The Sky is Not the Limit
What makes projects like the live satellite tracker so fascinating isn't just the sheer scale of the data; it's how accessible the technology has become. By leveraging classical physics models (SGP4), modern spatial indexing (H3), memory-efficient caching (Redis), and highly optimized GPU pipelines (Instanced WebGL), we can run calculations on our personal machines or lightweight cloud VMs that would have required supercomputers a few decades ago.
Whether you're building a live tracking dashboard, an IoT fleet management platform, or a massive multiplayer game, the architecture remains the same: offload spatial indexing to discrete global grids, cache hot data in-memory, and use parallel hardware rendering on the client.
What's your take?
Have you worked with spatial indexes like H3 or S2 in production? Or perhaps you've built complex real-time rendering pipelines in WebGL? Let me know in the comments below, and don't forget to subscribe to the newsletter for more deep dives into software architecture.
Until next time, keep coding!