Short answer: choose free tools when the team can operate and restore the control plane; choose paid control when delegated operations are worth the dependency, but in either case persist each flag decision beside the import run so rollback evidence survives.
A flag system is suitable for scheduled property imports only when its decision is observable and its rollback is boring. The useful comparison is not a free-versus-paid feature checklist; it is the boundary between a control plane that can be restored and an application path that can explain what happened.
For a property-management SaaS, I would require four invariants: an import run has a durable identity, every flag decision is attributable to a version and subject, an empty result is distinguishable from a successful empty dataset, and disabling a rollout cannot erase the evidence needed for reconciliation. Those rules matter more than the billing model.
What must remain true when an import goes quiet
A nightly lease or unit import can stop producing records while the scheduler itself continues to report success. A flag may have disabled the fetch, selected an empty tenant scope, or routed the worker to a new parser. The alert should describe that boundary rather than merely say that a process is alive.
The import record is therefore the primary object, and flag evaluation is an input to it. Store an immutable run ID, tenant ID, source cursor or watermark, flag-set version, evaluation reason, start time, finish time, record count, and terminal status. Do not put raw lease data into a flag context just to make a diagnostic query convenient. Logging guidance from OWASP treats sensitive data handling as a design concern, and property data deserves the same restraint.
A useful status model has more than success and failure:
-
completed_nonempty: records were accepted. -
completed_empty: the source validly returned no records. -
skipped_by_flag: the control intentionally prevented work. -
failed_after_start: the worker began and could not finish. -
missing_heartbeat: no terminal record arrived within the schedule's expectation.
The alert policy can then ask a precise question: did a run cross its expected completion window without a terminal status, or did it complete empty when the tenant's normal contract says that is suspicious? That is a rollback decision, not a dashboard decoration.
How do free tools and paid feature flags compare for rollback?
There are two broad choices: operate the flag evaluation and storage yourself, or use a managed control plane while keeping run evidence in your own system. Both can work. The deciding question is what remains available during a bad deployment, a control-plane outage, or a mistaken flag update.
| Decision boundary | Self-hosted flag service | Managed flag service |
|---|---|---|
| Evaluation during control-plane loss | Requires a tested local cache or embedded snapshot | Requires a tested SDK cache or snapshot policy |
| Rollback authority | Internal operators own storage, access, and procedure | Provider supplies the control surface; the team owns its emergency path |
| Audit evidence | Team must retain change records and evaluation metadata | Team must export or independently persist the evidence it needs |
| Operational burden | Patching, capacity, backups, and access controls are internal work | Service operations are delegated, while integration and policy remain internal |
| Exit test | Restore the snapshot and replay decisions locally | Prove the application can run from a known-good snapshot without live calls |
This is not a ranking. A small team may reasonably delegate control-plane operations, while a regulated environment may prefer tighter ownership of the storage and restore procedure. The non-negotiable part is the same: a flag lookup must not be the only copy of the decision that affected a financial or operational record.
The free-versus-paid distinction usually changes who carries operational work and which managed capabilities are available; it does not remove the need for local evidence. A self-hosted deployment can still fail its rollback test. A paid service can still leave an import unexplained if the application records only the final row count. The limitation of the self-hosted choice is operational ownership; the limitation of the managed choice is dependency on its availability and export boundary. Neither option is a fit for a team unwilling to test restoration.
How should a worker record a flag decision?
Keep the critical path small. Evaluate once for a run, persist the result before doing source work, and emit a structured completion event after the terminal state is known. The example uses generic interfaces so the rollback behavior is visible without binding the design to a product.
package importer import ( "context" "fmt" "time" ) type Decision struct { Enabled bool Version string Reason string } type Run struct { ID string TenantID string FlagVersion string FlagReason string Status string RecordCount int StartedAt time.Time FinishedAt time.Time } type Flags interface { Evaluate(ctx context.Context, tenantID, key string) (Decision, error) } type Runs interface { Create(ctx context.Context, run Run) error Finish(ctx context.Context, run Run) error } func Import(ctx context.Context, flags Flags, runs Runs, tenantID string, now time.Time) error { decision, err := flags.Evaluate(ctx, tenantID, "property-import-v2") if err != nil { return fmt.Errorf("evaluate import flag: %w", err) } run := Run{ ID: newRunID(), TenantID: tenantID, FlagVersion: decision.Version, FlagReason: decision.Reason, StartedAt: now, Status: "started", } if err := runs.Create(ctx, run); err != nil { return fmt.Errorf("create import run: %w", err) } if !decision.Enabled { run.Status = "skipped_by_flag" run.FinishedAt = time.Now().UTC() return runs.Finish(ctx, run) } count, err := fetchAndPersist(ctx, tenantID, run.ID) if err != nil { run.Status = "failed_after_start" run.FinishedAt = time.Now().UTC() _ = runs.Finish(ctx, run) return fmt.Errorf("import run %s: %w", run.ID, err) } run.RecordCount = count run.Status = "completed_nonempty" if count == 0 { run.Status = "completed_empty" } run.FinishedAt = time.Now().UTC() return runs.Finish(ctx, run) } The persistence boundary is deliberate. If the worker crashes after creating started but before finishing, a separate watchdog can classify the run as missing a heartbeat after the schedule-specific deadline. If it crashes after writing imported rows, the run ID and idempotency key let reconciliation distinguish a retry from a second business event. Exactly-once processing is an aspiration at the orchestration layer; the durable key and idempotent writes are what make retries safe.
The example uses a simple newRunID and generic storage methods because the important contract is the ordering. In production, make the run creation and the import's idempotency record compatible with the database transaction model. Do not claim completion when only the scheduler callback completed.
What should the alert contain?
An alert for a silent import should be actionable without exposing tenant records. Include the tenant identifier or an approved surrogate, schedule name, run ID, last terminal status, expected deadline, flag key, flag-set version, evaluation reason, and a link to the audit record. Exclude access tokens, source payloads, lease terms, and full property addresses. OpenTelemetry describes logs as a signal that can carry structured attributes and correlate with other telemetry; use that correlation to join the run, worker trace, and alert without copying sensitive business data into every event.
A useful event might look like this:
{ "event": "property_import_terminal", "run_id": "run_01JEXAMPLE", "tenant_ref": "tenant_742", "status": "completed_empty", "record_count": 0, "flag_key": "property-import-v2", "flag_version": "cfg-18", "flag_reason": "tenant_allowlist", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" } The exact attribute names are less important than stable semantics and a retention policy. Audit trails should answer who changed the control, what version was evaluated, which run consumed it, and what terminal result followed. They should not become a second source database.
Why not let the scheduler own the truth?
A scheduler heartbeat is a valid signal for worker liveness, but it is a poor substitute for import completion. The scheduler can enqueue a job successfully while the job is blocked on a flag, waiting on an upstream source, or failing before it writes its terminal record. Treating enqueue success as business success creates a false green path that rollback cannot repair.
The rejected design is a single dashboard metric such as imports_completed_total, with a flag state inspected only during an incident. It is cheap to start and useful for a low-risk prototype, but it loses the decision version and cannot tell an empty result from a skipped run. It becomes valid when the job has no tenant-specific consequences, the source is naturally idempotent, and the team has accepted that post-hoc explanation is unnecessary. Property imports usually do not meet that bar.
Test the failure boundaries, not only the happy path: restore an old flag snapshot, kill a worker after Create, deliver the same source page twice, return a valid empty source response, and revoke control-plane connectivity. Each test should leave a predictable run state and a reconciliation record. The rollback is complete only when the next import can be evaluated from the known-good state and the previous decision remains auditable.
The practical comparison is therefore architectural. Choose the control-plane arrangement that your team can restore, observe, and explain; then keep import evidence independent enough that a flag change cannot rewrite history.
References
- OpenTelemetry, “Logs: Concepts”: https://opentelemetry.io/docs/concepts/signals/logs/
- OWASP, “Logging Cheat Sheet”: https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html
