Version vs Snapshot Isolation for Batch Jobs

Almost every batch job in a utility estate is pointed at a version, and most of them should not be. The habit is understandable: versioning is how the network is edited, so a version feels like the correct way to address it. But a job that only reads gains nothing from a version and inherits all of its costs — contention with editors, results that move while the job runs, and a cleanup obligation. A read-only snapshot gives the opposite trade: no writes, no visibility of work in progress, and in exchange, inputs that do not move and no interference with anyone editing. This guide sets out how to choose, how to materialise a snapshot cadence that analytics can rely on, and what to do about the jobs that genuinely need both. It follows the isolation policy described in branch versioning and conflict resolution.

Environment Prerequisites

  • A reconciled default version with a predictable post rhythm, so a snapshot taken after a post represents a coherent state rather than a moment mid-merge.
  • Somewhere to materialise the snapshot — a read-only replica, a file geodatabase export, or a versioned read against a recorded moment. Any of the three works; what matters is that the moment is recorded with it.
  • Python 3.11 with arcpy and, for the analytical jobs themselves, geopandas where the work does not need the network solver.
  • A job inventory listing every scheduled process against the network, with whether it writes and whether it needs uncommitted edits. Building this list is usually the moment the problem becomes obvious.
  • A convention for stamping results with the snapshot moment they were computed from, so two reports that disagree can be reconciled by their inputs rather than argued about.

Schema-Aware Selection Protocol — Score Each Job Before You Point It Anywhere

  1. Does the job write? If yes, it needs a version, and it needs its own — never the default version during editing hours. This single question settles most of the inventory.
  2. Does it need to see uncommitted edits? Only validation inside an editor’s own version genuinely does. A job that “needs current data” almost always means “needs data as of the last post”, which is exactly what a snapshot provides.
  3. Must two runs agree? A report re-run for the same period must produce the same numbers. If it reads the live network, it will not, and the difference will be attributed to the report.
  4. How long does it run? A long read against the default version is the most common source of blocking in a versioned estate, and it is invisible in a write-path investigation.
  5. What does it publish? A job whose output drives an operational decision needs a recorded input moment for the same reason a trace does: without it, nobody can reconstruct what the decision was based on.
What a version gives a job, what a snapshot gives it, and what each costs A version can be written to, sees uncommitted edits, and participates in reconcile and post. It also holds locks, competes with editors, and has to be cleaned up. A snapshot cannot be written to and cannot see work in progress, but it is immune to concurrent editing, costs nothing to hold, and gives every consumer of a run identical inputs. Most batch jobs need the second set of properties and are given the first out of habit. Property Named version Reconciled snapshot Can be written yes no Sees uncommitted edits yes no Affected by concurrent editing yes — results move no Holds locks yes no Reproducible inputs no yes Cleanup required yes — delete after post no Two of these rows decide most cases: writing, and reproducibility.

Materialising a Snapshot Cadence

The mechanics are less important than the discipline: whatever produces the snapshot must record the moment it represents, and every consumer must carry that moment through to its output.

from __future__ import annotations

import logging
from dataclasses import dataclass
from datetime import datetime, timezone

import arcpy

logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
LOG = logging.getLogger("snapshot")


@dataclass(frozen=True)
class Snapshot:
    """A read-only view of the network with the moment it represents attached."""

    path: str
    moment: datetime
    source_version: str = "sde.DEFAULT"

    def stamp(self) -> dict:
        """The provenance block every result computed from this snapshot must carry."""
        return {
            "snapshot_path": self.path,
            "snapshot_moment": self.moment.isoformat(),
            "source_version": self.source_version,
        }


def materialise(workspace: str, out_gdb: str, datasets: list[str]) -> Snapshot:
    """Export a coherent read-only copy of the network and record its moment.

    The moment is taken before the export starts, not after: a consumer needs to know
    the earliest state the data could represent, so that a comparison against an edit
    log is conservative rather than optimistic.
    """
    moment = datetime.now(timezone.utc)
    arcpy.env.workspace = workspace
    for name in datasets:
        try:
            arcpy.management.Copy(name, f"{out_gdb}/{name.split('.')[-1]}")
            LOG.info("copied %s", name)
        except arcpy.ExecuteError as exc:
            LOG.error("copy failed for %s: %s", name, exc)
            raise
    return Snapshot(path=out_gdb, moment=moment)


def assert_fresh(snap: Snapshot, max_age_minutes: int) -> None:
    """Refuse to run an analysis against a snapshot older than the job tolerates."""
    age = (datetime.now(timezone.utc) - snap.moment).total_seconds() / 60
    if age > max_age_minutes:
        raise RuntimeError(
            f"snapshot is {age:.0f} minutes old, job tolerates {max_age_minutes}")
A snapshot cadence that serves analytics without touching the editing path The default version is reconciled and posted on its normal rhythm. A scheduled export then materialises a read-only snapshot — a replica, a file geodatabase, or a versioned read at a recorded moment — and stamps it with that moment. Every analytical job reads the snapshot rather than the live network, so two jobs run an hour apart still agree. When a job genuinely needs current data, it reads the default version directly and accepts that its results are not reproducible. POST default version advances EXPORT snapshot materialised moment recorded ANALYSE every batch job reads the snapshot STAMP results carry the snapshot moment exception NEEDS LIVE DATA reads DEFAULT directly and accepts that the run is not reproducible Two jobs an hour apart should agree; only a snapshot guarantees that.

assert_fresh is the part worth copying. A snapshot cadence fails quietly when the export job stops: every downstream analysis keeps running against an increasingly stale copy, and nothing reports a problem because the data is perfectly well formed. Making each consumer declare its tolerance turns a silent staleness into a loud failure in the job that cares.

Production Deployment Pattern

  1. Export the snapshot immediately after the post window. A snapshot taken mid-post can catch a partially applied change, which is the one state nobody wants to analyse.
  2. Point every read-only job at it, by default. Make the version the exception that has to be justified in the job’s configuration, not the default that is never questioned.
  3. Stamp every output with the snapshot moment. A report, an impact set, a compliance table — all of them carry the moment, so a disagreement between two of them is resolved by comparing inputs.
  4. Assert freshness in each consumer. Let the job that needs data less than an hour old fail loudly rather than silently computing on yesterday’s copy.
  5. Keep write jobs in their own versions and delete them. An automation version that outlives its run is the second most common source of reconcile slowness after abandoned project versions.
  6. Handle the jobs that need both. A few genuinely need to write and to see a stable input set — a bulk remediation that computes from a fixed picture and writes its corrections back. Run those as two phases: read the snapshot, compute the change set entirely in memory or in a staging table, then open a version and apply it. Splitting the read from the write keeps the version open for seconds rather than for the length of the analysis, which is where the contention would otherwise come from.
  7. Retire the habit deliberately. Existing jobs will keep using a version because that is how they were written, so the change has to be made job by job with the inventory as the checklist. Each conversion is small; the aggregate effect on contention is not.
  8. Measure where blocking actually happens. Before tuning the write path, count blocking events by the connection that caused them. On most estates the answer is long analytical reads, and the fix is this pattern rather than anything about editing.
Where contention actually comes from in a versioned estate Measured on an estate that moved its analytical workload off the editing path, almost all blocking came from long-running read jobs holding connections against the default version during the post window — not from editors, and not from the posts themselves. Moving those reads to a snapshot removed the contention without changing a single editing workflow. The shape of this result is common enough to be worth checking before any tuning effort is spent on the write path. share of blocking events, one estate, one month Batch reads on DEFAULT 71 % moved to a snapshot Concurrent posts 18 % sequenced into a window Editor sessions 11 % left alone The write path is usually not where the contention is.

Conclusion

The version-or-snapshot decision looks like an implementation detail and behaves like an architectural one. Writing jobs need a version of their own; reading jobs almost always want a snapshot, and giving them one removes most of the contention an estate attributes to versioning itself. The discipline that makes it work is recording the moment and carrying it through to every result, because a reproducible number is one whose inputs can be named. Once analytics run off the editing path, the reconcile and post loop has room to run on a schedule rather than in the gaps, which is what makes reconcile and post automation dependable.

For authoritative reference, consult the ArcGIS Pro versioning documentation and the Python datetime library.