Analytics & TestingCustomer Data PlatformsMarketing Tools

Haversine Formula: The Actual Distance Between Latitude and Longitude

A store locator, a sales territory, and a near me search all start with the same pair of numbers: a latitude and a longitude. Subtracting those numbers looks like distance. It is not. Latitude and longitude are angles on a curved surface, and degrees of longitude shrink as you move away from the equator. A geographic information system (GIS) that treats the pair as a flat grid will tell a customer in Minneapolis that a shop is close when the road, and the planet, say otherwise.

Takeaway: Use the haversine formula, or a database function that already implements a sphere or a spheroid. Do not subtract coordinates, and do not call a planar distance function on raw latitude and longitude.

A Straight Line Through Degrees Is Not a Distance

The Pythagorean formula, A² + B² = C², is the right tool on a flat sheet. Surveyors call that result the Euclidean distance. It answers a different question from the one a map pin asks. On Earth, the short path between two cities stays on the surface. A straight line through the coordinates either cuts through the crust or pretends the degree grid is a sheet of graph paper.

A degree of latitude is about 111 kilometers (69 miles) almost everywhere, because the north-south circles are nearly the same size. A degree of longitude is about 111 kilometers (69 miles) at the equator, about 84 kilometers (52 miles) at New York, and about 56 kilometers (35 miles) at 60 degrees north. The same one-degree step is a long drive in Ecuador and a shorter one in Oslo. Any formula that adds latitude change and longitude change as if they were the same unit will be wrong in one of those places, and often in both.

A second straight line also fails. Convert both cities to a 3D point and measure the chord through the interior. That chord is shorter than the trip a plane or a driver can actually take, because the traveler cannot pass through the planet. The figure below puts the two mistakes side by side: a straight stroke across a degree grid, and a chord through a sphere next to the arc that stays on the outside.

Diagram comparing a straight line across a latitude and longitude grid with the haversine arc along the surface of the Earth and the shorter chord through the planet.

The grid is only a rough filter, because equal degrees are not equal kilometers or miles. The arc on the right is the radius you want. The haversine formula turns the two latitudes and the two longitudes into that central angle.

The Great-Circle Route

The shortest path on a sphere is an arc of a great circle, a circle whose plane passes through the center of the Earth. On a flat map, the arc often bends toward the pole, which is why flight tracks look curved. A shorter radius is that same arc, stopped at the distance you chose.

This short film shows why the great circle and the straight line on the map are not the same journey.

Why don't ships go straight? | Great Circles

How the Haversine Formula Works

Latitude is the angle north or south of the equator. Longitude is the angle east or west of the prime meridian. The haversine of an angle is sin²(angle / 2). That half-angle form stays clear of the inverse cosine, which breaks when rounding pushes its input past 1.

In radians, with lat1 and lat2 as the latitudes and Δlat and Δlon as the differences:

Loading formula...

Loading formula...

Loading formula...

The value c is the central angle in radians. R is the radius you assigned to the sphere. Multiply them, and you have a length. An older and still common version uses the spherical law of cosines and then acos. The two are the same geometry. They are not the same numerically. When the cities are a few hundred meters (a few hundred yards) apart, the cosine of the angle is so close to 1 that floating-point rounding can push it to 1.0000002. The inverse cosine of a number past 1 is not a number. The haversine form uses a square root and atan2, which stay defined, and a clamp of a into the range 0 to 1 removes the last sliver of rounding error.

The radius is a choice. A mean Earth radius of 6,371.0088 kilometers (3,958.76 miles) is a sound default. The equator is about 6,378.137 kilometers (3,963.2 miles) from the center, and the poles are about 6,356.752 kilometers (3,949.9 miles). Use that same radius in the database if the function accepts one. MySQL’s default of 6,370,986 meters (3,958.7 miles) will otherwise disagree with application code by a fraction of a percent.

New York City at 40.7128, −74.0060 and Los Angeles at 34.0522, −118.2437 are 3,936 kilometers (2,446 miles) apart on that sphere. The chord through the planet is 3,873 kilometers (2,407 miles). Treating every degree as 111 kilometers (69 miles) produces about 4,975 kilometers (3,091 miles), more than 1,000 kilometers (about 650 miles) too long. Only the arc belongs in a radius search.

What the Usual Samples Get Wrong

A famous city pair will not catch every failure. A cross-country check can look fine while a metro query is off, or while the number is in degrees and the label says miles.

  • The formula is the law of cosines, not the haversine. acos of a cosine is unstable for short distances and can return NaN when rounding slips past 1.
  • JavaScript and Java call a round function that does not take a decimal count. Math.round ignores a second argument in JavaScript and does not have a two-argument form in Java. The code either doesn’t compile or silently returns an integer.
  • An unknown unit returns 0 or nothing. A typo of kilometer against kilometers looks like a distance of zero, which a map then treats as a hit at the origin.
  • MySQL ST_Distance on a geometry is planar. On latitude and longitude it returns degrees, not meters. Dividing that number by 1,609 does not produce miles. The spherical call is ST_Distance_Sphere, and it expects longitude first, then latitude.
  • The inserted points are swapped. POINT(40.7128, −74.0060) is a location in the Arctic, not New York. Longitude is the X coordinate.
  • A SQL Server call that measures a line to a point prints a raw float. Callers must know that 4326 means meters, which you divide by 1,609.344 to get miles, and that geography::Point takes latitude first, the opposite of well-known text.

Bill Karr, chief data scientist at OpenINSIGHTS, supplied the spherical law of cosines in Python. The functions below use the haversine, reject impossible coordinates, and refuse units they don’t recognize. Another resource pointed to SQL Server geography.STDistance. The call further down is point-to-point and specifies the unit.

A Function That Refuses Bad Input

Every language below uses the same contract. Latitude is between −90 and 90, longitude is between −180 and 180, and every input must be finite, because a NaN slips through a comparison. The unit is miles or kilometers. Anything else throws. The radius is 6,371.0088 kilometers (3,958.76 miles). The return value is rounded to two decimals for display. Keep the unrounded value if you sort or filter on it.

PHP on PHP 8 can express that contract with typed arguments. abs avoids a less-than comparison against the pole, and the clamp on a keeps the square root inside the reals.

function haversine_distance(float $lat1, float $lon1, float $lat2, float $lon2, string $unit = 'miles'): float {
    foreach (array($lat1, $lon1, $lat2, $lon2) as $value) {
        if (!is_finite($value)) {
            throw new InvalidArgumentException('Coordinates must be finite numbers.');
        }
    }
    if (abs($lat1) > 90 || abs($lat2) > 90) {
        throw new InvalidArgumentException('Latitude must be between -90 and 90.');
    }
    if (abs($lon1) > 180 || abs($lon2) > 180) {
        throw new InvalidArgumentException('Longitude must be between -180 and 180.');
    }
    $unit = strtolower($unit);
    if ($unit !== 'miles' && $unit !== 'kilometers') {
        throw new InvalidArgumentException('Unit must be miles or kilometers.');
    }

    $earth_km = 6371.0088;
    $lat1 = deg2rad($lat1);
    $lat2 = deg2rad($lat2);
    $dlat = $lat2 - $lat1;
    $dlon = deg2rad($lon2 - $lon1);
    $sin_lat = sin($dlat / 2);
    $sin_lon = sin($dlon / 2);
    $a = $sin_lat * $sin_lat + cos($lat1) * cos($lat2) * $sin_lon * $sin_lon;
    $a = min(1.0, max(0.0, $a));
    $km = $earth_km * (2 * atan2(sqrt($a), sqrt(1 - $a)));
    $distance = $unit === 'miles' ? $km / 1.609344 : $km;
    return round($distance, 2);
}

JavaScript rounds with a scale-and-divide, because Math.round ignores a second argument. An unknown unit throws instead of returning undefined.

function haversineDistance(lat1, lon1, lat2, lon2, unit = 'miles') {
  const values = [lat1, lon1, lat2, lon2];
  if (values.some((value) => typeof value !== 'number' || !Number.isFinite(value))) {
    throw new TypeError('Coordinates must be finite numbers.');
  }
  if (Math.abs(lat1) > 90 || Math.abs(lat2) > 90) {
    throw new RangeError('Latitude must be between -90 and 90.');
  }
  if (Math.abs(lon1) > 180 || Math.abs(lon2) > 180) {
    throw new RangeError('Longitude must be between -180 and 180.');
  }
  if (unit !== 'miles' && unit !== 'kilometers') {
    throw new RangeError('Unit must be miles or kilometers.');
  }

  const earthKm = 6371.0088;
  const toRad = (degrees) => degrees * Math.PI / 180;
  const dLat = toRad(lat2 - lat1);
  const dLon = toRad(lon2 - lon1);
  const rLat1 = toRad(lat1);
  const rLat2 = toRad(lat2);
  const sinLat = Math.sin(dLat / 2);
  const sinLon = Math.sin(dLon / 2);
  const a = Math.min(1, Math.max(0,
    sinLat * sinLat + Math.cos(rLat1) * Math.cos(rLat2) * sinLon * sinLon
  ));
  const km = earthKm * (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)));
  const distance = unit === 'miles' ? km / 1.609344 : km;
  return Math.round(distance * 100) / 100;
}

Python uses the math module only. Importing round from NumPy hides the built-in. A bad coordinate or unit raises ValueError.

from math import atan2, cos, radians, sin, sqrt

def haversine_distance(lat1, lon1, lat2, lon2, unit='miles'):
    values = (lat1, lon1, lat2, lon2)
    if any(isinstance(value, bool) or not isinstance(value, (int, float)) for value in values):
        raise TypeError('Coordinates must be finite numbers.')
    if any(value != value or value in (float('inf'), float('-inf')) for value in values):
        raise ValueError('Coordinates must be finite numbers.')
    if abs(lat1) > 90 or abs(lat2) > 90:
        raise ValueError('Latitude must be between -90 and 90.')
    if abs(lon1) > 180 or abs(lon2) > 180:
        raise ValueError('Longitude must be between -180 and 180.')
    if unit not in ('miles', 'kilometers'):
        raise ValueError('Unit must be miles or kilometers.')

    earth_km = 6371.0088
    lat1, lat2 = radians(lat1), radians(lat2)
    dlat = lat2 - lat1
    dlon = radians(lon2 - lon1)
    a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
    a = min(1.0, max(0.0, a))
    km = earth_km * (2 * atan2(sqrt(a), sqrt(1 - a)))
    distance = km / 1.609344 if unit == 'miles' else km
    return round(distance, 2)

Java rounds with a scale-and-divide, because Math.round has no decimal-place argument. An unknown unit throws IllegalArgumentException instead of returning 0.

public static double haversineDistance(double lat1, double lon1, double lat2, double lon2, String unit) {
    if (!Double.isFinite(lat1) || !Double.isFinite(lon1) || !Double.isFinite(lat2) || !Double.isFinite(lon2)) {
        throw new IllegalArgumentException("Coordinates must be finite numbers.");
    }
    if (Math.abs(lat1) > 90 || Math.abs(lat2) > 90) {
        throw new IllegalArgumentException("Latitude must be between -90 and 90.");
    }
    if (Math.abs(lon1) > 180 || Math.abs(lon2) > 180) {
        throw new IllegalArgumentException("Longitude must be between -180 and 180.");
    }
    if (!"miles".equals(unit) && !"kilometers".equals(unit)) {
        throw new IllegalArgumentException("Unit must be miles or kilometers.");
    }

    double earthKm = 6371.0088;
    double rLat1 = Math.toRadians(lat1);
    double rLat2 = Math.toRadians(lat2);
    double dLat = rLat2 - rLat1;
    double dLon = Math.toRadians(lon2 - lon1);
    double sinLat = Math.sin(dLat / 2);
    double sinLon = Math.sin(dLon / 2);
    double a = sinLat * sinLat + Math.cos(rLat1) * Math.cos(rLat2) * sinLon * sinLon;
    a = Math.min(1.0, Math.max(0.0, a));
    double km = earthKm * (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)));
    double distance = "miles".equals(unit) ? km / 1.609344 : km;
    return Math.round(distance * 100.0) / 100.0;
}

Systems That Already Compute the Distance

Write the formula when you are teaching it or when the language has no spatial library. In a database, call the function the engine already indexes. Several products say distance and mean three different models.

MySQL and MariaDB expose ST_Distance_Sphere, in meters, with longitude first. Divide that result by 1,000 for kilometers or by 1,609.344 for miles. ST_Distance on a geometry is planar, and on raw latitude and longitude that means degrees. PostgreSQL with PostGIS uses the spheroid by default and a faster sphere when you pass false. SQL Server geography.STDistance is a close geodesic, not a textbook haversine.

STDistance() returns the shortest LineString between two geography types. This is a close approximate to the geodesic distance. The deviation of STDistance() on common earth models from the exact geodesic distance is no more than .25%.

Microsoft, STDistance for the geography type

BigQuery ST_DISTANCE on a GEOGRAPHY value is a spheroidal geodesic, in meters, so a hand-rolled radius will not match it exactly. Elasticsearch uses the haversine for geo_point distances. MongoDB does spherical work on a 2dsphere index. The older 2d index is a flat plane. GeoJSON stores longitude first.

In application code, Turf.js distance states that it uses the haversine. GeoPy offers great_circle for the sphere and geodesic for the ellipsoid, and geodesic is the library default. The Google Maps JavaScript geometry library computes a spherical distance in spherical.computeDistanceBetween, with a default radius of 6,378,137 meters (3,963.2 miles). Use these when you want a maintained implementation. Use the function in this article when you need the radius, the unit, and the error handling to match a system you control.

SystemCallWhat It Measures
MySQL and MariaDBST_Distance_SphereSphere, in meters. Default radius 6,370,986 m (3,958.7 miles). Longitude, then latitude.
MySQL geometryST_DistancePlanar distance in coordinate units. Degrees, if you passed raw latitude and longitude.
PostGIS geographyST_DistanceSpheroid by default, in meters. Pass false for a faster sphere.
SQL Server geographySTDistanceApproximate geodesic. Meters when the spatial reference is 4326.
BigQueryST_DISTANCESpheroidal geodesic, in meters. Not a haversine you can reproduce with one radius.
Elasticsearchgeo_distance, ST_DISTANCEHaversine on a geo_point.
MongoDB2dsphere, $geoNearSpherical. The legacy 2d index is planar.
Turf.jsdistanceHaversine.
GeoPygreat_circle or geodesicSphere, or ellipsoid. The default distance is geodesic.
Google Mapsspherical.computeDistanceBetweenSphere. Default radius 6,378,137 m (3,963.2 miles).

A Radius Query That Can Use an Index

A haversine in the WHERE clause, applied to every row, cannot use an ordinary index. The practical pattern is a bounding box first, then the exact distance on the rows that survive. Store latitude and longitude as decimal columns for the box, and store a point for the spherical function. In MySQL 8, the point should use spatial reference 4326, with longitude as the first coordinate.

CREATE TABLE locations (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    latitude DECIMAL(9,6) NOT NULL,
    longitude DECIMAL(9,6) NOT NULL,
    coordinates POINT NOT NULL SRID 4326,
    SPATIAL INDEX (coordinates),
    KEY lat_lon (latitude, longitude)
);

Insert New York with the axes in the order the sphere function expects. The decimal columns keep the human order, latitude then longitude, so a reviewer can see the city. The point does not.

INSERT INTO locations (name, latitude, longitude, coordinates)
VALUES (
    'New York City',
    40.712800,
    -74.006000,
    ST_SRID(POINT(-74.006000, 40.712800), 4326)
);

The radius query passes the center as bound parameters. Nothing in the SQL text is built by concatenating a request. The third argument to ST_Distance_Sphere is the same radius the application uses, in meters, so the two layers agree. The box is in degrees: about 111.195 kilometers (69.09 miles) per degree of latitude, and that value times the cosine of the latitude for longitude. Near the poles, the cosine collapses, so the application should drop the longitude filter above about 89 degrees and scan the latitude band only.

SELECT id, name,
    ST_Distance_Sphere(
        coordinates,
        ST_SRID(POINT(?, ?), 4326),
        6371008.8
    ) / 1000 AS distance_km
FROM locations
WHERE latitude BETWEEN ? AND ?
  AND longitude BETWEEN ? AND ?
HAVING distance_km <= ?
ORDER BY distance_km;

Bind longitude, latitude, the four edges of the box, and the radius in kilometers, in that order. A prepared statement is the control. A radius or a browser geolocation is still untrusted input, and it must not be concatenated into the query text.

PostGIS can skip the hand-rolled sphere when you want the spheroid. Leave the third argument off, or pass true, and the result is meters on the spheroid for spatial reference 4326. Pass false only when you are matching a haversine elsewhere.

SELECT ST_Distance(
    ST_GeogFromText('SRID=4326;POINT(-74.0060 40.7128)'),
    ST_GeogFromText('SRID=4326;POINT(-118.2437 34.0522)')
);

SQL Server takes latitude first in geography::Point, then longitude, then 4326. The return value is meters. Well-known text on the same type is longitude first. Mixing those two orders is the bug that places New York in the wrong ocean.

DECLARE @here geography = geography::Point(40.7128, -74.0060, 4326);
DECLARE @there geography = geography::Point(34.0522, -118.2437, 4326);
SELECT @here.STDistance(@there) AS meters;

When the Sphere Is Not Enough

A sphere is enough for a store radius, a territory, and most marketing distances. The gap versus an ellipsoid is usually a fraction of a percent, smaller than the error in a rooftop geocode. Use a geodesic when the length itself is the product: a survey, a pipeline, or a regulated flight distance. PostGIS geography without the sphere switch, GeographicLib, and GeoPy’s geodesic path use the World Geodetic System 1984 ellipsoid. Karney’s algorithm is the current choice. Vincenty’s formula fails to converge on some near-antipodal pairs.

Write the unit next to the number, and keep the radius or the ellipsoid name in the code. The haversine does not fix a swapped axis, a concatenated query, or a unit the function does not know. Those checks are what make the formula safe to ship.

Related Articles