Limits and Failure Cases
This page documents situations where RecoverLand does not work, works partially, or may produce an incorrect result. Each case includes an explanation of the cause and a recommendation to avoid it.
Severity scale
| Level | Meaning |
|---|---|
| CRITICAL | Data loss or restore on the wrong feature. The user must understand this risk before using RecoverLand in this context. |
| MODERATE | The system works but with reduced reliability. History remains browsable but automatic restore is not recommended. |
| LOW | Rare edge case. Impact limited to a specific scenario. |
File moved or renamed
MODERATE
The problem
The layer fingerprint uses the absolute path of the source file. If the file is moved, renamed, or copied elsewhere, the new path produces a different fingerprint. The history recorded under the old path is no longer linked to the layer.
What 6.0.0 changed, and what it did not
Version 6.0.0 added an alias mechanism: the datasource_alias table links an obsolete fingerprint to the current one, and every read path expands a fingerprint into all of its historical forms. That mechanism repairs the fingerprints the plugin used to compute incorrectly. It does not repair a file that has actually moved.
The migration recomputes the canonical fingerprint from the source URI recorded in datasource_registry at capture time. For a moved file that URI still holds the old path, so the recomputed value equals the stored one, no alias row is written, and the source is simply counted as already canonical.
Two neighbouring cases are fixed and no longer fork the history of a file:
- A display filter (
Layer > Filter, which QGIS appends as|subset=...) or an opening option (|geometrytype=...). Only the identifying keyslayernameandlayeridtake part in the identity now, so filtering a layer no longer starts a parallel history that a filter-off rewind never sees. - A
spatialiteconnection string or adelimitedtextURI, which used to be passed throughabspathand therefore depended on the directory QGIS was launched from.
Consequence
The existing history remains intact and browsable (it is not deleted). But RecoverLand does not know that the moved file is the same as the old one. Searching by layer does not show the old history, and no action in the dialogs re-attaches it: the alias table is written by the migration, never by the interface. The add_alias function is part of the plugin's public Python API, so an alias can be written by hand from the QGIS console, but that is not a supported workflow: the pair you give it is refused only when it is empty, self-referential, or would close a cycle. Nothing checks that the two fingerprints designate the same data.
How to avoid it
- Do not move data files after you have started editing them in QGIS
- If you must reorganize folders, do it before starting editing work
- Use stable paths (no USB keys, no temporary folders)
Network drive with variable mount point
MODERATE
The problem
By default the fingerprint of a file source contains its absolute path. If a file is accessible as Z:\share\vegetation.shp on one workstation and as \\server\share\vegetation.shp on another, the two paths produce different fingerprints even though they point to the same physical file.
Same problem if the drive letter changes: Z: one day, Y: the next.
Partial mitigation: project-relative fingerprints
The environment variable RECOVERLAND_FINGERPRINT_MODE=relative makes the fingerprint of file-based sources (ogr, spatialite, delimitedtext) carry the path relative to the QGIS project home instead of the absolute path. When the project file and its data live under the same share, the mount point disappears from the fingerprint and the same history is recognised whatever letter the drive was given.
What that mode does not do:
- It is off by default. Any other value, or no value at all, means absolute mode.
- It is an environment variable of the QGIS process, not a project setting. It has to be set on every workstation, and set before capture starts.
- If the project has no home path (a project never saved) or if the data sits on a different drive from the project, no relative form exists: the plugin falls back to the absolute path and writes a WARNING in the log.
- Turning it on later does not re-attach a journal already written under absolute paths. The two forms are recognised as the same layer when RecoverLand looks for the layer an event belongs to, or checks whether a layer is being tracked; the history query is widened only through alias rows, and no alias is created for a mode change.
How to avoid it
- Always use the same path to access the file (always the drive letter, or always the UNC path)
- Fix the network drive letter so it does not change between sessions
- Or keep the project file and its data under one share, and set
RECOVERLAND_FINGERPRINT_MODE=relativeon every workstation before the first capture
Unstable FID (shapefile, GeoJSON)
MODERATE
The problem
The FID of a shapefile is the record number in the .dbf file. This number can change if:
- The shapefile is re-exported ("Save As..." in QGIS or another tool)
- The .dbf file is packed after deletions
- An external script reorganizes the records
What happens now
Until 6.0.0 the consequence was corruption: you recorded a change on feature FID=42 (a building), the shapefile was re-exported, FID 42 became a road, and the restore wrote the building's attributes onto the road while reporting success. That is fixed. Before writing on a target reached by FID alone, the restore has to prove the target is the feature the event describes:
- if a primary key resolved the target, the FID never entered the picture and there is nothing to check;
- if the event captured a post-edit geometry, the feature sitting at that FID must still carry it;
- for an attribute-only edit, the feature sitting at that FID must still carry the new value of every field the event changed.
When none of those can be established, the restore looks for the feature elsewhere in the layer, by the state the event captured after the edit. If that search finds nothing, RecoverLand writes nothing, and says in the log why the action was not applied. Refusing loses a restore; writing would overwrite a feature that is not the one the event describes.
Undoing an INSERT is itself a delete
Undoing an INSERT does not overwrite a feature, it removes one : and on a layer with no primary key that decision rests on a resemblance rather than on an identity. The weakest resemblance the engine still accepts is a single point in common between the feature sitting at the recorded FID and the state the event captured: the geometry, when the event carries one, or one captured attribute still holding its captured value (_occupant_shares_evidence, core/restore_executor.py).
How to avoid it
- Use a format that can carry a real primary key (GeoPackage, SpatiaLite, PostGIS) instead of a shapefile : but see the caveat below: the label RecoverLand puts on a source is not proof that a key exists
- If you must use a shapefile, do not re-export it between captures and restores
- Read the report of a restore, and read it in the log panel (
View > Panels > Log Messages, RecoverLand tab): a refused action names its reason there
"Strong identity" is read off the file extension
refine_ogr_identity in core/support_policy.py decides the identity strength of an OGR source by testing its URI, lowercased, against a series of substrings and returning on the first one that matches: .gpkg, .sqlite, .db and .fgb are called strong, a handful of others medium or weak. The rule worth knowing is the last line of the function rather than the list: a URI that matches nothing in it : any extension the function does not enumerate : is called medium, the same label a shapefile gets. Nothing in that function asks the provider whether a primary key exists.
What actually decides is elsewhere. compute_feature_identity in core/identity.py writes pk_field / pk_value into the recorded identity only when dataProvider().pkAttributeIndexes() is non-empty and the field carries a non-null value on that feature; otherwise the identity is {"fid": N} and nothing else. The restore proof reads that recorded identity, never the strength label.
Recovery horizon without a primary key
CRITICAL
The problem
When a layer's provider declares no primary key, the identity RecoverLand records for a feature is the FID alone: {"fid": 412}. There is nothing else to record.
OGR reuses the FIDs freed by a deletion. Every deletion followed by a compaction puts a different feature under a number that journal entries already refer to. The journal is not wrong : it says what it saw : but the number it holds stops designating the feature it described.
This is not the re-export accident documented above, which happens at one point in time and which you can decide to avoid. It is cumulative and it is silent. Ordinary editing consumes the ability to go back: the further back a rewind reaches, the larger the share of its actions that address FIDs since handed to another feature. Capture keeps working, the journal keeps growing, search keeps returning results. The loss only becomes visible at the moment of the rewind.
What it looks like on a real project
One rewind measured on a real project on 2026-08-12 gave this breakdown:
| Outcome of the 73 planned actions | Counter | Count |
|---|---|---|
| Restored | applied | 6 |
| Already in the target state, nothing to do | skipped_idempotent | 3 |
| Nothing written back: the captured geometry was not valid, and repairing it moved it too far | failed_geometry_drift | 5 |
| Failed: the target could not be established | failed_target_absent | 59 |
Fifty-nine actions out of seventy-three ended with no feature the restore was willing to act on. The events were all present; what the run could not do is establish which feature each of them designates today. Those five numbers : 73, 6, 3, 5, 59 : are the ones pinned in the versioned scenario scripts/validation/scenarios/rw_cycle_summary_truth.py, which replays that breakdown through the real runner. All the repository keeps of that run is that one CYCLE_SUMMARY line of counters: how many layers the rewind covered, and what kind of identity their events carried, are not in it, so this page does not state them. What the scenario checks is that the summary line adds up (the run that produced these counters had announced failed=0 next to 59 failures); it does not re-execute the restore against a layer, so treat the table as one measurement, not as a repeatable benchmark.
failed_geometry_drift has a single producer, _buffer_update in core/restore_executor.py, and it does not mean "the geometry changed since the capture". It fires when the geometry the restore is about to write back is not GEOS-valid: makeValid() repairs it, and when the repaired shape has moved from the original by more than MAKEVALID_DRIFT_TOLERANCE (a value in the layer’s CRS units, core/constants.py) the geometry is not written, rather than putting on the map a shape the user never had. The attributes are written after the geometry, so that action writes nothing at all: the feature is left exactly as the restore found it, and the run counts the action under failed_geometry_drift.
failed_target_absent aggregates three refusals (_classify_restore_result in core/restore_service.py): no feature found at the recorded identity, an occupant that could not be proved to be the right one, and a target the update path declined to verify. FID recycling is the cause this section is about, and it fits the shape of the measured run : but a feature genuinely deleted since capture, a layer re-exported, or a feature whose geometry and captured attributes both changed since the capture, all land in the same bucket. Nothing in the journal separates them. Read 59 as "59 actions found nothing they could prove", not as "59 recycled FIDs".Why RecoverLand cannot repair this for you
The FID is the only identity the layer offers. Rebuilding a lost link would mean guessing which of the current features is the one the event described, and a wrong guess overwrites an unrelated feature : the exact failure this page exists to prevent. So the engine does not rebuild the link: it reports that it could not find the feature.
View > Panels > Log Messages, RecoverLand tab). The CYCLE_SUMMARY line that carries the four counters of the table above goes further still: to the debug file only, <QGIS profile>/recoverland/recoverland_debug.log, with a copy in <project>/.recoverland/recoverland_debug.log. A user who never opens either sees a rewind end with no visible verdict at all.What a primary key changes, and what it does not
When the provider declares a primary key, RecoverLand records it next to the FID (pk_field / pk_value) and resolves the target with it. If the key is not found, the restore stops there and never falls back to the FID, precisely because the FID may point at an unrelated feature. A primary key removes this cause of degradation: the identity travels with the row, and deletions elsewhere in the table cost nothing.
now (UTC) − retention_days. retention_days defaults to 365 and is settable between 7 and 3650 days in the Maintenance dialog, alongside a max_events cap (default 1 000 000) that removes the oldest events beyond that count. Startup auto-purge is off by default, so nothing is deleted until you enable it or run a purge yourself : but once an event is purged it is gone. The time cursor of a rewind cannot be taken back further than the oldest event the journal still holds, so purged history is simply out of reach.How to reduce the exposure
- Give the layer a primary key the provider actually declares: an integer primary key on a GeoPackage or SpatiaLite table, or PostGIS. This is the only measure that removes the cause instead of delaying it. Changing container format without declaring a key changes only the label RecoverLand prints : see "Strong identity" is read off the file extension.
- On a layer with no primary key, treat the journal as a short-term safety net: check and restore soon after the incident, not months later.
- Do not compact or re-export such a layer between a capture and a restore.
- After every rewind, open the Log Messages panel for the counts, and the debug file for the
CYCLE_SUMMARYline behind them.failed_target_absentmeasures how much of that history the engine could no longer tie to a feature; nothing is written for those actions.
FID = -1 (feature being created)
LOW
The problem
During feature creation (before commit), QGIS assigns a temporary FID of -1. If the plugin captures a snapshot at this moment, the recorded identity is {"fid": -1}, which is not unique.
Real impact
This case is rare because RecoverLand captures at commit time (afterCommitChanges), not during editing. At that point, QGIS has already assigned a definitive FID. The risk is theoretical.
Pre-6.0.0 database history left unattached
MODERATE
The problem
Before 6.0.0, the fingerprint of a PostgreSQL, MS SQL or Oracle source captured the schema where it meant the table. Every table of one schema therefore shared a single identity and their histories were merged in the journal.
6.0.0 computes the fingerprint correctly, and the v6 migration attaches the obsolete fingerprints to the new ones by writing alias rows : without rewriting a single journal event. Read paths expand a fingerprint into all of its historical forms, so an old journal stays usable after the upgrade.
That repair is impossible in one case: when an old fingerprint would have to split into several current ones. The journal holds one merged history, several tables claim it, and nothing in the data says which events belong to which table. The migration refuses to guess. It writes no alias row for that source, and the old events stay filed under the merged fingerprint.
How the migration decides
The probe only runs on database providers, and only when the table part of the fingerprint actually moves. It then asks how many distinct QGIS layer identifiers were ever recorded under that fingerprint:
- more than one : the fingerprint covered several layers: ambiguous, no alias;
- exactly one : the fingerprint names one source: the alias is written;
- none recorded at all (journal written by a very old build) : the layer names are used as a fallback, and more than one name is again ambiguous.
The same "leave it alone" outcome applies to a fingerprint the journal has no source address for, and to a pass that could not finish. A pass that did not complete is not marked done: it is retried the next time the project is opened.
Consequence
No event is lost and nothing is rewritten : deleting the alias rows restores the pre-migration state exactly. But a rewind scoped on the current identity of such a table does not reach the events captured under the merged fingerprint.
qlog, which writes to the debug file and to the QGIS Log Messages panel (View > Panels > Log Messages, RecoverLand tab). Nothing appears in the message bar, and nothing appears in the RecoverLand window itself. The warning is emitted once per journal when the RecoverLand dialog refreshes its layer list : not on project opening alone, so a user who never opens the dialog is never told : and again every time a rewind is launched on a degraded journal. If you see that message, treat the older history of the named layers as consultable but not restorable through a rewind on the current identity.Changes outside QGIS
CRITICAL
The problem
RecoverLand listens to QGIS edit signals. If a file is modified by another application (ArcGIS, Python script, text editor, ogr2ogr), RecoverLand detects nothing. These changes are not in the journal.
How to avoid it
Make all your changes in QGIS. If you must use a script, run it in the QGIS Python console in edit mode (startEditing / commitChanges) so that the signals are emitted.
Recording disabled
CRITICAL
If the user disables the recording toggle, no change is captured. RecoverLand cannot recover what was never recorded. There is no catch-up mode.
What the status bar indicator says
The indicator is computed from runtime facts (tracker, writer thread, outbox, journal, disk), not from a saved setting. Its states, in the order they win:
- ERROR: edits are, or were this session, not recorded anywhere: writer thread dead, events lost, capture cut by the disk monitor, event generation failed for a commit
- PAUSED: capture is off (your toggle, or the memory pressure pause after a very large commit)
- DEGRADED: edits are durable but not through the normal path: journal temporarily unwritable (edits wait in the outbox), outbox unwritable (edits written straight to the journal), low disk, records waiting in the quarantine, layers edited that cannot be captured
- PERSISTING: healthy, the writer is indexing events that are already durable
- PROTECTED: every accepted edit is in the journal or durably on its way
Every transition that puts history at risk is also pushed once to the QGIS message bar (then at most once every 2 minutes for the same cause), and written to the Log Messages panel and to the structured log.
Disk full or critically low
CRITICAL
The problem
If the disk hosting the journal drops below 100 MB free, RecoverLand automatically disables tracking. Changes made after that point are not captured.
How it is handled
The DiskMonitor module checks disk space when the journal is opened and every 5 minutes after that. When the critical threshold is reached:
- Recording is automatically disabled, and a persistent setting records that the disk monitor is the one who disabled it
- A message giving the volume and the free space left is written to the QGIS Log Messages panel, at ERROR level, and the status bar indicator is refreshed
- At the warning threshold (500 MB), the same channel gets a WARNING-level notice, once; recording continues
- A volume that cannot be measured at all : an unplugged share, a drive letter that disappeared : is treated as critical, not as healthy
Recovery
Free up disk space. The monitor re-enables recording on its own: at the next check : up to 5 minutes later, or immediately if the journal is reopened : if free space is back above the warning threshold of 500 MB and the setting says the disk monitor is the one who disabled recording, tracking is switched back on and a line saying so is written to the log.
tracking_disabled_by_disk marker, so the re-arming branch of the periodic check returns without doing anything and the monitor will not switch recording back on for you (recover.py). In that case the toggle is yours to switch back on. It can still switch it off: the critical branch disables recording whenever the check finds free space below the critical threshold and tracking active, whoever turned it on. What the monitor gives up after your toggle is the re-arming, not the cut.QGIS crash, power cut
LOW
What is still lost
Changes that were never committed (still in the edit buffer when QGIS stops) are not captured: the provider never received them either, so the data and the history agree.
What is not lost any more
Before RecoverLand says a commit is recorded, the commit is on disk in the capture outbox, an append-only file next to the journal (<journal>.outbox/), written and flushed with fsync on the calling thread. The SQLite journal is filled from it afterwards by the writer thread. This costs time on the GUI thread at every commit, in proportion to the number of features changed (measured by scripts/validation/bench_capture.py and bench_durability.py).
- Crash during the provider commit: the state of the features before the edit is written to the outbox before the provider starts writing. At the next start the commit is recorded as
INTERRUPTEDin the commit ledger (capture_commit) with that pre-edit state, compared with what the provider really holds, and reported. Nothing is inferred as "not applied". - Crash after the commit, before indexing: the outbox is replayed at the next start. Replay is idempotent (every event carries an
event_uid): an event already indexed is counted, never duplicated. - Torn write (crash in the middle of an append): every complete line is replayed; the cut line is moved to the quarantine and reported, never deleted. A batch whose end marker is missing is recorded
INCOMPLETE. - Power cut or operating system crash: the outbox copy of a batch is released only after the journal commit is itself synchronised (
synchronous=FULLon the writer connection). A commit that SQLite has not yet written through keeps its outbox copy.
What you see after such a restart
A message bar item states how many edits were brought back from the outbox, how many commits were interrupted (with the layers to check), and how many lines were quarantined.
Partial commit
MEDIUM
The problem
A QGIS commit is written in stages: geometries, attributes, deletions, additions. A stage that fails does not undo the stages already written: commitChanges() returns False while part of the edit is in the file (measured on GeoPackage with a UNIQUE constraint refusing an addition; the same holds for any provider that writes each stage on its own).
What RecoverLand does
The stages that the provider reported as written are journaled, with their pre-edit state, and the commit is recorded PARTIAL. A message bar item says that the commit was partial. When you fix the problem and validate again, the rest is journaled with the next commit; when you roll back, what was already written stays journaled (it is in the file).
What RecoverLand writes itself
A strict restore or rewind that meets a partial commit reads the file back, undoes the stages it had written, and reports the whole layer as refused (REFUSED_COMPENSATED). If the undo itself fails, or cannot be attempted (Shapefile and GeoJSON renumber their features, so what was written cannot be mapped back safely), the verdict is REFUSED_COMPENSATION_FAILED: the writes that stayed in the file are traced in the journal, counted as applied, kept in the undo scope, and named in the report.
Automatic transaction groups
CRITICAL
The problem
With the project option "Automatically create transaction groups where possible" (PostgreSQL, GeoPackage), QGIS sends every edit to the database immediately, inside a database transaction, and the usual commit signals carry nothing. RecoverLand cannot see the pre-edit state or the changes.
What RecoverLand does
Such a layer is not captured. When it enters edit mode, a message bar item says so and the indicator switches to DEGRADED for the session.
Schema drift
MODERATE
The problem
If a layer's structure changes between capture and restore (field added, removed, renamed, or type changed), the restore may fail or produce an incomplete result.
What RecoverLand does
Before restore, the schema_drift.py module compares the saved schema with the current schema and generates a compatibility report. Missing or incompatible fields are flagged.
Concurrent editing
LOW
RecoverLand is designed for single-workstation use. If two users edit the same file simultaneously, each user has their own journal. A restore by one user may overwrite the other's changes without warning.
This scenario is not supported. For collaborative editing, use a database with a locking system (PostGIS, for example).
Memory layer
LOW
A memory layer (a QGIS scratch layer) has no persistent source file and no stable feature identity. RecoverLand does not capture it at all.
The provider policy in core/support_policy.py gives memory an identity strength of NONE, with capture and restore both off. EditSessionTracker.connect_layer refuses every layer whose strength is NONE and returns before connecting a single edit signal, logging action=refused reason=no_stable_identity. No signal is bound, so no commit is ever seen and no event is ever written.
flog (core/edit_tracker.py), which writes to the debug file recoverland_debug.log and to nothing else. It does not reach the Log Messages panel, so the panel staying empty is not a sign the layer is being captured. Save a scratch layer to a real file before you start doing work in it you would mind losing.Quarantine
MEDIUM
What it is
Records that were accepted but that the journal refuses (a value SQLite rejects), and lines of the outbox cut by a crash, are written to <journal>.outbox/quarantine/, synchronised, and never deleted by RecoverLand. Each event is either in the journal or in the quarantine, never both: a replay skips the events the quarantine already holds.
What you see
As long as the quarantine holds records, the indicator stays DEGRADED with their count, including after a restart. The files are JSON Lines: one record per line, with the reason and the full event.
Null and empty geometries
LOW
A feature without geometry (NULL) and a feature with an empty geometry (for example POINT EMPTY) are two different states, and the journal keeps them apart: a geometry that appears or disappears is recorded with an explicit marker, so a rewind removes a geometry added after the target date and gives back one removed after it.
The check made after a restore commit treats NULL and EMPTY as the same "no shape" value, because some providers store one as the other (a Shapefile has no empty geometry). A restore that should give back EMPTY and finds NULL, or the reverse, is reported as applied.
Journal size
LOW
The problem
The journal grows indefinitely if retention is not configured. On a project with thousands of daily changes, the file can reach several hundred MB.
What the plugin actually measures
RecoverLand publishes no query-latency measurement, and this page will not invent one. What exists is a classification: evaluate_journal_health in core/health_monitor.py grades the journal on file size and event count and keeps the worse of the two grades.
| Grade | Reached at | What it produces |
|---|---|---|
| healthy | below 50 MB and below 100,000 events | nothing |
| info | 50 MB or 100,000 events | the temporal coverage of the journal, and no suggestion |
| warning | 200 MB or 500,000 events | "journal growing, consider purging", plus a pointer to the Maintenance dialog |
| critical | 500 MB or 1,000,000 events | "large journal, purge recommended" |
These are thresholds someone chose, not the point at which anything was observed to slow down. Crossing 100,000 events changes a label from healthy to info; it does not mean a query became slower there.
Solution
Use the Maintenance dialog to configure a retention policy. For example: keep 90 days of history and purge the rest.
Impact of delta storage
RecoverLand uses delta storage for UPDATE: only the attributes that actually changed are recorded, so changing one field of a fifty-column table stores one value and not fifty. No per-event size is published here, estimated or measured: it depends entirely on how many columns a table has, how many of them an edit touches, and whether the geometry changed. What the plugin does measure is the file itself, through the grades above.
Journal deleted
CRITICAL
If the .recoverland/recoverland_audit.sqlite file is deleted (manually, by a disk cleanup tool, or by an antivirus), the entire history is permanently lost. RecoverLand will create a new empty journal at the next startup.
QGIS project moved
MODERATE
If you move a QGIS project (.qgz) to another folder, the project_fingerprint changes. The existing journal (which is in the .recoverland/ folder at the old location) is no longer found automatically.
How to avoid it
If you move a project, also move the .recoverland/ folder that is next to the .qgz file. The journal will be found if .recoverland is present next to the project file.
Orphan journals (unsaved projects)
LOW
The situation
When a project is never saved, its journal lives in the QGIS profile directory under a hashed filename (audit_<hash>.sqlite). If the project is closed without saving, this journal file becomes an orphan: it exists on disk but is never opened again.
Automatic cleanup
At startup, the JournalManager scans the unsaved-project audit directory and removes orphan journals older than 30 days (excluding the currently active journal). This cleanup is automatic and silent.
What RecoverLand is not
| RecoverLand is not... | Use instead... |
|---|---|
| A versioning system (no branches, no merge) | Git, GeoGig, or a dedicated versioning system |
| A backup system (does not copy files) | Classic file backup, rsync, or a NAS |
| A collaboration system (no sync between workstations) | PostGIS with session management, or a collaborative GIS |
| A real-time monitoring system (no push notifications) | Database triggers or a DBMS-level audit system |
| A Ctrl+Z replacement (only works after commit) | The native QGIS Ctrl+Z for uncommitted changes |
What RecoverLand is
A local safety net. It automatically records every committed change in a SQLite journal, and allows you to find and restore previous states. It protects against common human errors: accidental deletion, incorrect attribute modification, geometry overwrite.
It works best with strong-identity formats (GeoPackage, PostGIS), on a single workstation, with files that remain at the same location.