Running a Local Overpass Instance for Bulk Queries Jump to heading
Stand up your own Overpass endpoint from a regional extract so a bulk workload runs at whatever rate your hardware allows, without taking slots from the shared public servers.
Prerequisites Jump to heading
Conceptual minimum Jump to heading
An Overpass instance is a purpose-built database, not a wrapper over a PBF file. The import pass reads the extract and writes a set of index structures — by element id, by tag, and by geographic cell — into a database directory. Queries are then answered from those indexes. Two consequences follow.
The first is that the import is the expensive part and it is a one-off. It is write-heavy, largely single-threaded in its later stages, and dominated by disk. Running it on a network volume or a small burst-credit disk turns hours into days. The second is that an instance is only as fresh as its last update. A freshly imported instance reflects the extract’s timestamp, and without an update loop it silently ages exactly the way a stale extract does — a failure mode covered in OSM Replication & Diff Sync.
Area filters deserve special mention. Areas are not stored in the source data; the Overpass software derives them in a separate pass over boundary relations and closed ways. That pass is optional and it is not cheap, so a minimal instance answers element queries perfectly and returns nothing for every area query. If your workload uses (area.x) filters — the ones built in Writing Overpass QL Area and Bounding Box Queries — you must enable and schedule area generation, and you must wait for its first run before those queries work.
Runnable solution Jump to heading
The container images maintained by the Overpass community handle the import and the update loop together; the work is in configuring them deliberately rather than accepting defaults.
#!/usr/bin/env bash
# Stand up a private Overpass instance from a regional extract.
set -euo pipefail
EXTRACT_URL="https://download.geofabrik.de/europe/poland-latest.osm.pbf"
# The replication directory MUST match the region the extract covers.
UPDATE_URL="https://download.geofabrik.de/europe/poland-updates/"
DB_DIR="/srv/overpass/db"
META="yes" # keep version/timestamp/user so `out meta` works
AREAS="yes" # run the area-generation pass; needed for (area.x) filters
mkdir -p "$DB_DIR"
docker run -d --name overpass \
-e OVERPASS_MODE=init \
-e OVERPASS_PLANET_URL="$EXTRACT_URL" \
-e OVERPASS_DIFF_URL="$UPDATE_URL" \
-e OVERPASS_META="$META" \
-e OVERPASS_RULES_LOAD=10 \
-e OVERPASS_UPDATE_SLEEP=60 \
-e OVERPASS_ALLOW_DUPLICATE_QUERIES=yes \
-v "$DB_DIR:/db" \
-p 12345:80 \
wiktorn/overpass-api
# The import runs inside the container and can take hours. Watch it:
docker logs -f overpass
Once the import finishes, verify the instance answers a query you already know the answer to:
from __future__ import annotations
import logging
import requests
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger("osm.overpass.local")
LOCAL = "http://localhost:12345/api/interpreter"
PUBLIC = "https://overpass-api.de/api/interpreter"
HEADERS = {"User-Agent": "osm-pipeline-example/1.0 (contact@example.org)"}
COUNT_QUERY = (
"[out:json][timeout:60];"
'node["amenity"="pharmacy"](50.02,19.87,50.10,20.05);'
"out count;"
)
AREA_QUERY = (
"[out:json][timeout:60];"
'area["name"="Kraków"]["admin_level"="8"]->.a;'
'node["amenity"="pharmacy"](area.a);'
"out count;"
)
def count(endpoint: str, query: str) -> int:
response = requests.post(endpoint, data={"data": query},
headers=HEADERS, timeout=180)
response.raise_for_status()
tags = response.json()["elements"][0]["tags"]
return int(tags["total"])
def compare() -> None:
local_bbox = count(LOCAL, COUNT_QUERY)
public_bbox = count(PUBLIC, COUNT_QUERY)
logger.info("bbox query: local=%d public=%d", local_bbox, public_bbox)
if abs(local_bbox - public_bbox) > max(2, public_bbox * 0.02):
logger.warning("counts diverge by more than 2%% — check import freshness")
local_area = count(LOCAL, AREA_QUERY)
if local_area == 0 and public_bbox > 0:
logger.error("area query returned nothing: area generation has not run")
else:
logger.info("area query: local=%d", local_area)
if __name__ == "__main__":
compare()
Step-by-step walkthrough Jump to heading
- Match the replication directory to the extract. The update URL must serve diffs for exactly the region the extract covers. A country extract updated from a continent’s diff directory applies changes for territory the database does not contain, and the mismatch is not detected for you.
- Decide about metadata before importing, not after. Keeping version, timestamp and user roughly doubles the index size but is the only way
out metaworks. Changing your mind means a full re-import. - Enable area generation explicitly. It is a separate pass with its own schedule. Without it, every
(area.x)filter returns an empty set, and — as the parent topic warns — an empty area produces no error. - Give the import a real disk. The import is dominated by random writes. Local NVMe turns a multi-hour import into a manageable one; a network volume can turn it into an overnight job.
- Set the update interval deliberately. A sixty-second sleep gives near-minutely freshness at the cost of continuous background work. If your consumers are happy with hourly data, a longer interval leaves far more capacity for queries.
- Compare against the public endpoint once. The verification script runs the same bounding-box query against both and warns when the counts diverge by more than a couple of percent, which is the signal that the import is stale or incomplete.
- Test an area query separately. A zero result from the area query while the bounding-box query returns hundreds is the unambiguous signature of area generation not having run.
Verification Jump to heading
- The bounding-box count matches the public endpoint. Within a couple of percent; a larger gap means the extract predates recent edits or the import did not complete.
- An area query returns a non-zero count. If it returns zero while the same features are found by bounding box, area generation has not run.
out metareturns version fields. Query one known element without metaand confirmversionandtimestampare present; if they are not, the import discarded metadata.- The update loop advances. Check the instance’s replication state after an hour; the sequence number must have increased.
- A representative workload query completes. Run the slowest query from your real workload and record its duration, so you have a baseline to compare against after the next import.
Common errors and fixes Jump to heading
| Symptom | Root cause | One-line fix |
|---|---|---|
| Every area query returns zero | Area generation never ran | Enable the area pass and wait for its first completion |
out meta returns no version |
Imported without metadata | Re-import with metadata enabled; it cannot be added later |
| Import fails near the end | Disk exhausted | Provision around ten times the extract size before starting |
| Counts drift from the public endpoint | Update loop stopped or wrong diff URL | Match the replication directory to the extract’s region |
| Queries slow after weeks of updates | Index fragmentation from continuous diffs | Schedule a periodic re-import rather than updating forever |
| Import takes more than a day | Network-attached or burst-credit disk | Import on local NVMe, then move the database directory |
| Instance answers, results look truncated | Extract covers less than the query area | Query only inside the region the extract covers |
Specification reference Jump to heading
The Overpass database is built by an import pass that writes element, tag and geographic indexes into a database directory, and areas are produced by a separate
osm3sarea-generation rule pass that must be scheduled independently of the main import. Replication diffs are applied by an update loop that tracks its own sequence state. See the Overpass API installation documentation for the stages, their ordering, and the metadata options fixed at import time.
Frequently Asked Questions Jump to heading
How much disk does a private Overpass instance actually need?
Plan for roughly ten times the source extract size once metadata, generated areas and update headroom are included. The raw element and tag indexes alone are around four times the extract, metadata adds most of the rest, and the update loop needs working space. Densely mapped regions sit at the top of that range. Running out of space part way through an import means restarting it from the beginning, so headroom is cheaper than the retry.
Why do my area filters return nothing on a fresh instance?
Because areas are derived by a separate generation pass that does not run as part of the main import. Until that pass has completed at least once, no area objects exist, and an area filter over an empty set returns an empty result with no error. Enable area generation explicitly, schedule it, and confirm a known area query returns a non-zero count before you rely on any query that uses an area filter.
Do I need minutely updates, or is a periodic re-import enough?
It depends entirely on what your consumers need. An update loop keeps the instance within minutes of the live map but runs continuously and slowly fragments the indexes, so long-lived instances get gradually slower. A weekly or monthly re-import from a fresh extract is simpler to operate, gives a predictable performance profile, and is entirely adequate when the questions being asked do not depend on very recent edits.
Can I import a country extract and query outside it?
No, and the failure is quiet. A query whose bounding box extends past the extract’s coverage returns whatever exists inside the imported region and nothing for the rest, with no indication that part of the answer is missing. Either import a region that covers every query you will make, or add an explicit guard that rejects queries whose bounds fall outside the imported area.
Related Jump to heading
- Overpass API Query Language — the query model a private instance answers identically to the public one.
- Handling Overpass Timeouts and Rate Limits — the client discipline that becomes optional once you own the server.
- Building a Minutely Update Pipeline — the replication machinery the update loop is doing on your behalf.
- Replication Sequence Numbers & State — how to read the state the instance keeps.
- OSM Extract Providers & Automated Downloads — sourcing the extract this import starts from.
Up one level: Overpass API Query Language.