Applying Minutely Diffs to a PostGIS Database Jump to heading
Keep an osm2pgsql- or imposm-loaded PostGIS database current by appending minutely OSM change files incrementally, so only the rows that actually changed are touched — never a multi-hour full reimport.
Prerequisites Jump to heading
Conceptual minimum Jump to heading
A file-based update rewrites the entire .osm.pbf for every diff; a database-based update instead applies the diff’s create/modify/delete operations as row-level INSERT/UPDATE/DELETE against the rendered tables, so cost tracks the number of changed objects rather than database size. That is why a minutely cadence — impractical against a large file, as the parent guide Applying .osc Change Files with osmium notes — is entirely comfortable against PostGIS.
The load-bearing requirement is slim mode. To turn a diff’s modify way 12345 v8 into the right SQL, the updater must know that way’s previous geometry and which rendered rows it produced — it must resolve node references to coordinates and remember prior state. osm2pgsql --slim persists that bookkeeping in the planet_osm_nodes, planet_osm_ways, and planet_osm_rels tables; without them the tool has no way to reconstruct the delta and simply refuses to append. imposm keeps the equivalent state in its own cache directory. Either way, the update path is: fetch the ordered diffs, apply them with --append, and advance the recorded sequence only after the database transaction commits — the same atomic-at-the-state-boundary discipline the OSM Replication & Diff Sync pillar insists on.
Runnable solution Jump to heading
The orchestration below fetches the next span of minutely diffs with pyosmium-get-changes, appends them with osm2pgsql, and advances a sequence file only when both the fetch and the append succeed. It shells out to the two CLIs so the exact flags used in a manual run are visible and reproducible.
from __future__ import annotations
import logging
import subprocess
from pathlib import Path
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger("osm.pg_update")
REPL_URL = "https://planet.openstreetmap.org/replication/minute/"
SEQ_FILE = Path("state.seq") # last committed sequence
DIFF_FILE = Path("update.osc.gz") # merged diff for this cycle
STYLE = Path("openstreetmap-carto.style")
DB = "osm"
def read_sequence() -> int:
return int(SEQ_FILE.read_text().strip())
def fetch_diffs(start: int, max_diffs: int = 120) -> int | None:
"""Fetch up to max_diffs minutely diffs after `start` into one .osc.gz.
Returns the new head sequence, or None when already current.
"""
result = subprocess.run(
["pyosmium-get-changes", "--server", REPL_URL,
"--start-id", str(start), "--size", str(max_diffs),
"-o", str(DIFF_FILE), "--verbose"],
capture_output=True, text=True,
)
if result.returncode == 3: # pyosmium: nothing new to fetch
logger.info("no new diffs after sequence %d", start)
return None
if result.returncode != 0:
raise RuntimeError(f"fetch failed: {result.stderr.strip()}")
# pyosmium-get-changes writes the reached sequence to a sidecar .osc.gz.seq
return int(Path(str(DIFF_FILE) + ".seq").read_text().strip())
def append_to_postgis() -> None:
"""Apply the fetched diff to PostGIS in slim append mode (single txn)."""
subprocess.run(
["osm2pgsql", "--append", "--slim",
"--database", DB, "--style", str(STYLE),
"--number-processes", "4", str(DIFF_FILE)],
check=True,
)
def run_once() -> None:
start = read_sequence()
head = fetch_diffs(start)
if head is None:
return
append_to_postgis() # raises on any non-zero exit
SEQ_FILE.write_text(str(head)) # advance ONLY after append committed
logger.info("PostGIS updated through sequence %d", head)
if __name__ == "__main__":
run_once()
For an imposm-managed schema the append step is a single command instead — imposm tracks its own diff state in its cache and reads the replication URL from its config:
# imposm equivalent: run continuous minutely updates against its own cache.
imposm run -config imposm.json -connection "postgis://osm@localhost/osm"
Step-by-step walkthrough Jump to heading
- Read the committed sequence.
read_sequenceloads the last sequence that was fully applied and committed, not merely fetched — the file is the single source of truth for where the database stands. - Fetch a bounded span.
fetch_diffscaps the request atmax_diffsminutely diffs, so a run that fell behind catches up in bounded chunks; return code 3 ispyosmium-get-changes’s “already current” signal and exits cleanly. - Append in slim mode.
append_to_postgisrunsosm2pgsql --append --slim, which reads the slim bookkeeping tables to turn each diff operation into the correct row-level SQL against the rendered tables.--number-processesparallelizes geometry building. - Commit then advance.
osm2pgsqlapplies the diff in its own transaction; only after it returns success doesrun_oncewrite the new sequence. A crash before the write leaves the database behind the sequence file’s prior value, which is safe because re-fetching and re-applying is idempotent. - Schedule the loop. Fire
run_onceevery minute from cron or a timer; the scheduling and locking details are the subject of Building a Minutely Update Pipeline, including how to prevent two runs overlapping.
Verification Jump to heading
- Sequence file moved. After a cycle,
state.seqholds a higher number and matches the.osc.gz.seqsidecar the fetch wrote. - Row counts changed.
SELECT count(*) FROM planet_osm_point;before and after should differ on an active region; a static count means nothing was appended. - A known edit landed. Pick a recently edited object id and query
planet_osm_line/planet_osm_polygonfor it; its geometry or tags should reflect the change. - No slim-mode error. The absence of
Cannot apply diffs to a database that was not imported with --slimin the log confirms the base import was slim. - Timestamps track live. The
osm2pgsqlreplication status (orimposm’s log) should report a lag of a few minutes at most under a minutely schedule.
Common errors and fixes Jump to heading
| Symptom | Root cause | One-line fix |
|---|---|---|
Cannot apply diffs ... not imported with --slim |
Base import ran without --slim |
Reimport once with --slim; updates then work in place |
| Sequence advances but rows unchanged | Advanced state before append committed | Write state.seq only after osm2pgsql exits 0 |
osm2pgsql exits with a lock error |
A previous run still holds the database | Serialize runs with a lock; never overlap cycles |
| Geometry missing after update | --style differs from the original import |
Reuse the exact style file the base import used |
| Fetch returns nothing repeatedly | --start-id past the stream head |
Confirm the stored sequence is not ahead of state.txt |
| Disk fills during append | Slim tables plus WAL growth | Provision for ~2x import size; tune PostgreSQL checkpoints |
| Updates fall progressively behind | Per-diff overhead exceeds one minute | Raise --number-processes or batch several diffs per cycle |
Specification reference Jump to heading
Incremental updates require the slim schema and the append mode. See the official osm2pgsql updating documentation for the
--append --slimrequirement and the middle-table bookkeeping, and the imposm3 documentation for its equivalent diff-update workflow and cache. The replication directory andstate.txtformat the fetch reads are documented on the OSM Wiki “Planet.osm/diffs” page.
Frequently Asked Questions Jump to heading
Why must the database be imported in slim mode to apply diffs?
Applying a modify or delete requires knowing an object’s previous state — its node coordinates and which rendered rows it produced. osm2pgsql --slim persists that bookkeeping in middle tables; without them the tool cannot reconstruct the delta and refuses to append. A non-slim import can only ever be fully reimported, never updated in place.
Can I apply minutely diffs to a file instead of a database?
You can, but every diff rewrites the entire PBF, so a minutely cadence against a large file spends almost all its time on I/O. A database applies only the changed rows, so update cost tracks change volume rather than database size — which is exactly why PostGIS suits minutely tracking and a file does not.
What happens if the update loop crashes mid-cycle?
If osm2pgsql fails or the process dies before the sequence file is written, the database stays at the prior committed sequence. The next run re-fetches from that sequence and re-applies, which is idempotent under version-numbered whole-object replacement, so no data is lost or double-counted. Advancing the sequence only after commit is what guarantees this.
Should I use osm2pgsql or imposm for updates?
Both apply diffs incrementally and keep their own bookkeeping — osm2pgsql in slim middle tables, imposm in a cache directory. Choose osm2pgsql when your rendering stack already depends on its schema and style files; choose imposm when you want its custom mapping configuration and built-in continuous run mode. The diff semantics are identical.
Related Jump to heading
- Applying .osc Change Files with osmium — the diff-application semantics that also govern the database path.
- Catching Up a Stale OSM Extract with pyosmium — the file-based counterpart and the fetch tooling reused here.
- Replication Sequence Numbers and State — where the database’s tracked sequence comes from.
- Building a Minutely Update Pipeline — scheduling and locking the loop shown here.
- Spatial Indexing for OSM Extracts — keeping spatial indexes current as rows change.
Up one level: Applying .osc Change Files with osmium.