Skip to content

feat(api): DB-backed scaled background execution backend + langflow worker - #13508

Open
ogabrielluiz wants to merge 95 commits into
release-1.14.0from
feat/v2-workflows-bg-scaled
Open

ogabrielluiz wants to merge 95 commits into
release-1.14.0from
feat/v2-workflows-bg-scaled

Conversation

@ogabrielluiz

@ogabrielluiz ogabrielluiz commented Jun 4, 2026 •

Copy link
Copy Markdown
Contributor

Adds the opt-in scaled backend so background flows run off the API workers, decoupled and distributed across replicas. Same BackgroundExecutionService facade, the endpoint never knows which backend is active. The queue is the database itself: no broker, no redis, so deployments need nothing beyond the DB the API already uses.

This sits directly on the release branch. The facade refactor that makes the default in-process path a first-class backend (#15018) is stacked on top of it rather than underneath.

What it adds

  • A langflow worker CLI process that lease-claims QUEUED job rows off the shared database (claim_next_queued_lease, the same exact-heartbeat conditional UPDATE every other single-flight guard uses), runs the same runner the default backend uses, and persists durable milestones to job_events so any API replica can reattach to any job.
  • A lease + heartbeat + periodic watchdog: a worker stamps a heartbeat while it runs, and a dead worker's in-flight job is reconciled by the watchdog without needing a restart. Reconciliation is liveness-aware, so a booting or scaled-up worker never re-claims or fails a job another worker is mid-running, and the worker_lost transition is a conditional UPDATE so concurrent watchdogs cannot write duplicate terminal events.
  • Cross-replica reattach: replay the durable job_events from the DB, then poll the same table past the replayed boundary (no gap, no double delivery, Last-Event-ID contract preserved). A grace pass on terminal status covers the paths that flip the status before appending their terminal milestone. Tails end on SUSPENDED too, so a HITL pause never strands a reader.
  • Stop is the durable execution_signals STOP row that the worker polls at vertex boundaries.
  • A composite (status, type, created_timestamp) index backing the claim poll and watchdog scan. ix_job_status_created, which shipped with the job metrics, cannot serve either query because the type filter sits between its two sorted columns.
  • Docs for running the fleet: a "Scale background runs" section, a background settings reference table (including the job retention window, which stays off until an operator sets one), and the troubleshooting entries for a multi-worker deployment.

Because the queue and the system of record are one table, the broker failure modes are structurally impossible: there is no pending/processing list to drift from the DB (a QUEUED row is enqueued by definition, so stranded-queue recovery has no equivalent) and no token-exchange protocol (the conditional UPDATE is the single flight).

Selection is the explicit LANGFLOW_BACKGROUND_BACKEND=scaled setting, decoupled from job_queue_type (redis for v1 build events no longer implies anything about the background backend). The facade logs once at wiring time in scaled mode so a fleet with zero workers is diagnosable. Scaled + SQLite works on a single host; use Postgres for multi-host fleets.

Semantics

Same guarantees as the default backend, now across processes: at-least-once for not-yet-started work, at-most-once for in-flight work by default (a crashed worker's job becomes FAILED worker_lost), and opt-in bounded retry per flow with atomic attempt accounting so concurrent watchdogs can't push a job past max_attempts.

Tradeoffs made deliberately: claim latency is bounded by the worker poll window (--idle-block-ms, default 1s) and reattach milestone latency by background_poll_interval_s (default 0.5s) instead of a broker's push; inline request globals are dropped on every scaled run (the worker hydrates from the redacted persisted row, so background runs need stored global variables).

What's proven

The former real-redis test fleet now runs against the ordinary test DB (no external service), and the real-service tier needs only Postgres:

  • A real langflow worker OS subprocess claims, builds, and completes a real graph off a shared Postgres while the API side reattaches; a kill -9 mid-job is reconciled by the watchdog (fresh-lease guard proven first); a durable stop reaches the separate worker and lands CANCELLED with the signal consumed.
  • Concurrency proofs: racing claimers get distinct jobs, racing watchdogs bump retry attempts exactly once and write exactly one terminal run_failed event, racing lease-claims on one row produce exactly one winner.
  • 249 background-execution tests green on real Postgres with zero skips; migration consistency green on SQLite and Postgres with a single alembic head.

Three review rounds (specialist + adversarial validation per round) ran on this rework; all findings are fixed in-branch. The redis-era Locust numbers were redone against this backend on real Postgres: the claim path holds single-flight at every fleet size tested (1 to 16 workers), the composite claim-scan index is load-bearing (claim latency degrades 48x without it), and event tails stay lossless through 1000 concurrent jobs. Head-to-head under saturation the scaled backend sustains 14 to 17 completions/s at 50 to 200 concurrent users with zero lost jobs, while the in-process default collapses to 1.4/s with 35% of submissions never completing (about 10x). Known ceiling at this stage: FIFO candidate contention rises with worker count (90.9% lost-claim rate at 16 workers with candidates=5). SKIP LOCKED is the marked upgrade path, and the rest of the P0 hardening list is worker-loop claim resilience plus the worker-fleet gauges. Retention/purge and the queue metrics that were on that list have since shipped on the release branch (#15316, #15314), so this branch carries the claim-scan index against a table that retention now bounds.

Summary by CodeRabbit

  • New Features

    • Added a database-backed scaled background execution mode.
    • Added a worker command for processing queued jobs in separate processes.
    • Added durable job events, cancellation signals, lease recovery, and watchdog handling.
    • Added explicit settings for backend selection and polling intervals.
    • Added load-testing support for background workflows.
  • Bug Fixes

    • Improved recovery of jobs interrupted by worker failures.
    • Ensured cancellation requests are persisted and honored reliably.
  • Tests

    • Added comprehensive coverage for worker execution, recovery, cancellation, concurrency, and end-to-end workflows.

Rebased onto release-1.10.0. The base independently rebuilt the v2
workflows backend (RBAC, body globals, share-aware fetch); keep our
forward design and conform its auth to that work:

1. Auth: keep get_current_user_for_workflow (session-or-API-key authN
   that does not hold a DB connection during the inline run, avoiding
   the SQLite lock contention api_key_security would cause) and enforce
   the base's RBAC on top: ensure_flow_permission(EXECUTE) before run,
   (READ) before status reconstruct, with widen_for_shares fetch.
2. Port the base's request-body globals onto the v2 WorkflowRunRequest.
   The X-LANGFLOW-GLOBAL-VAR-* headers stay supported (the Responses API
   passes globals that way); body globals win on conflict. Converters
   echo the effective globals via effective_globals.
3. Public endpoint keeps the v1 build_public_tmp posture
   (access_type==PUBLIC, run-as-owner); RBAC applies to the
   authenticated endpoint only.
4. Preserve the base's post-build KB-cache invalidation in the AG-UI
   build path.

The endpoint, AG-UI bridge, pluggable stream adapters, public endpoint,
and re-attach are unchanged.
The synchronous /api/v2/workflows response keyed every result under its
component id, so reading the answer meant knowing an id you can't predict.
Surface two additive fields:

- output_text: the flow's single text answer (ChatOutput/TextOutput). None
  when the flow has zero or multiple text outputs, so callers read outputs
  rather than the shortcut guessing which channel is the answer.
- session_id: echoes the resolved session so chat/memory callers can
  continue the same thread (v1 /run returned this; v2 had dropped it).

outputs is unchanged, so this is non-breaking.
…ponse

Pin the sync-response shortcuts on the v2 workflows endpoint:
- output_text surfaces the lone ChatOutput/TextOutput text and stays None for
  non-output message nodes, data-only flows, and multi-text flows
- session_id echoes the resolved session; the error response exposes neither
- each outputs entry exposes only {type, status, content, metadata}, with the
  component id carried by the dict key

Also drop the component_id kwarg the converter passed to ComponentOutput, which
has no such field and silently dropped it.
Replace the flat output_text shortcut with an `output` object carrying the
resolved text answer plus a `reason` that explains why it resolved that way
(single/multiple/none/non_string/failed), so a null answer is always
diagnosable instead of silently None. `reason` follows the LLM-domain
finish_reason/stop_reason convention, distinct from the lifecycle status.

Also add `display_name` to each ComponentOutput (the stable component id
stays the dict key) and a computed `has_errors` flag derived from errors.
Let a sync caller name the output(s) they want via output_ids so
output.text resolves deterministically (reason=single) on multi-output
flows instead of going null. Selection is steer-only: it picks the
answer among the named outputs without filtering the outputs map.

Invalid ids are rejected with 422 before the flow runs (and before any
job row is created), so a typo costs no compute. Resolution considers
selected outputs that actually fired, so branching flows resolve to
whichever candidate ran.
Give v2-workflows sync and the langflow stream protocol one parser. The
stream now emits a normalized "output" event per terminal output carrying
an OutputEvent (the ComponentOutput shape sync returns in outputs[id], plus
component_id). A shared build_component_output() backs both the sync
converter and the adapter, and the build loop ships authoritative vertex
metadata as an additive output_meta key on end_vertex (existing consumers
read build_data and ignore it).

This is access-pattern parity (one parser, same fields, same terminal set),
not byte-identical content: the stream reuses the v1 build path whose
display serialization differs from sync's run_graph output.
…ackend)

Turns v2 mode:background into a durable, in-API background execution service behind a BackgroundExecutionService facade. Adds the store layer (result/error columns, job_events durable milestone log, execution_signals control, heartbeat/lease, 3 migrations), the default backend (bounded executor, runner, in-memory live bus, liveness-aware single-flight orphan sweep), the v2 endpoint rewiring, and the real-instance test harness. Needs no new infra; works on the SQLite single-process install. The redis-scaled worker backend is stacked on top in a follow-up PR.
@coderabbitai

coderabbitai Bot commented Jun 4, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

The saved review history does not include the base for the last reviewed commit. This saved history cannot establish the base for an incremental review. Comment @coderabbitai full review to establish a new review baseline. No full review was started, and the last reviewed checkpoint was preserved.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

The PR replaces Redis-based scaled background execution with a PostgreSQL-backed queue. It adds explicit backend settings, lease-based workers, durable event streaming, watchdog recovery, a worker CLI command, migrations, load testing, and real-service validation.

Changes

Scaled background execution

Layer / File(s) Summary
Runtime and job contracts
src/lfx/src/lfx/services/settings/groups/runtime.py, src/backend/base/langflow/services/jobs/service.py, src/backend/base/langflow/services/database/models/jobs/model.py, src/backend/base/langflow/alembic/versions/...
Scaled execution is selected explicitly. Job claiming, lease recovery, atomic failure, and the claim-scan index are added.
Database backend and facade wiring
src/backend/base/langflow/services/background_execution/db_backend.py, factory.py, service.py, live_bus.py, src/backend/tests/unit/services/background_execution/test_db_backend.py, test_lease_watchdog.py, test_scaled_end_to_end.py, test_scaled_stop.py
DBBackgroundQueue persists stop signals, claims jobs, reconciles stale leases, and streams durable events. The service and factory use it for scaled execution.
Worker process execution
src/backend/base/langflow/__main__.py, src/backend/base/langflow/services/background_execution/worker.py, src/backend/tests/unit/services/background_execution/test_worker_*.py, _subprocess_harness.py
The langflow worker command runs database-backed jobs with lease heartbeats, watchdog reconciliation, signal handling, request hydration, and teardown.
Operational validation and load testing
.github/workflows/migration-validation.yml, src/backend/tests/locust/v2_background_locustfile.py, src/backend/tests/unit/background_execution/*
CI and real-service tests use PostgreSQL without Redis. Side-effect safety, subprocess recovery, and background workflow load testing are added.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested labels: refactor

Suggested reviewers: erichare

Sequence Diagram(s)

sequenceDiagram
  participant API as BackgroundExecutionService
  participant DB as PostgreSQL
  participant Worker as langflow worker
  participant Runner as WorkerJobRunner
  API->>DB: Store queued job
  Worker->>DB: Claim lease
  Worker->>Runner: Hydrate and execute job
  Runner->>DB: Store status and events
  API->>DB: Replay durable events
Loading
Pre-merge checks: Passed 6 · Failed 3

Failed Failed checks (3 warnings)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 35.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Test Quality And Coverage Warning Core queue, runner, stop, lease, replay, and API success/error paths are tested, but periodic watchdog and CLI signal paths lack tests; retry safety runs the graph directly, not through the worker. Add tests for run_worker_loop with watchdog_interval_s, CLI backend and signal handling, migration index creation, and retry-safe side effects through actual claim and worker execution.
Test File Naming And Structure Warning The subprocess and end-to-end tests are under tests/unit; the project marks that path as unit, and no changed file uses pytest.mark.integration. Move true integration/E2E tests to tests/integration or add the integration marker under an agreed directory; add try/finally cleanup for created worker tasks.
Passed Passed checks (6 passed)
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Test Coverage For New Implementations Passed Added focused DB queue, lease watchdog, worker, factory, stop, settings, and facade tests, plus scaled end-to-end and real langflow worker subprocess tests; files use test_*.py.
Excessive Mock Usage Warning Passed Changed background-execution tests use no Mock or patch APIs; real database fixtures and worker subprocesses cover core behavior, with only small contract-focused fakes and environment monkeypatching.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main changes: a database-backed scaled background execution backend and the new langflow worker command.
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v2-workflows-bg-scaled

Comment @coderabbitai help to get the list of available commands.

@ogabrielluiz

Copy link
Copy Markdown
Contributor Author

Stacked PR 2 of 2, based on #13507 (durable default backend). Review #13507 first. Combined view: #13505.

@github-actions

github-actions Bot commented Jun 4, 2026 •

Copy link
Copy Markdown
Contributor

✅ Migration Validation Passed

All migrations follow the Expand-Contract pattern correctly.

The hard_proof marker name was a vibe word that said nothing about what the
tests need. Rename it to real_services everywhere: the pytest marker
registration, the *_hard_proof.py test files, the Makefile target
(real_services_tests), the -m selector in migration-validation.yml, and the
CI job. real_services says what these tests require: real Postgres + Redis +
worker subprocesses. (integration was already taken for the external-API
suite under tests/integration.)
…rker

Stacks on the default backend. Adds the opt-in redis backend: a langflow worker CLI claiming jobs off a redis list, a redis Streams live bus for cross-replica reattach, and a lease+heartbeat+periodic watchdog (liveness-aware reconcile, atomic retry accounting). Same facade; default behavior unchanged when no redis is configured. Proven on real redis incl. a real worker subprocess, kill-9 watchdog reconcile, and a Locust load test (22,841 req / 0 failures / ~2.5x throughput vs in-process). Includes the v2 background locustfile.
…ting prints

The events.quitting listener took _environment as a required positional
arg, but locust passes environment as a keyword, so the handler raised
TypeError at shutdown and the submitted/completed summary never printed
(locust exited 2). Accept the event kwargs via **_kwargs.
… in scaled worker tests

Follows the marker rename on the base PR: updates the scaled worker
subprocess tests and the subprocess harness to the real_services marker.
@ogabrielluiz
ogabrielluiz force-pushed the feat/v2-workflows-bg-scaled branch from e2ccbaf to 2f961f5 Compare June 6, 2026 02:48
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jun 6, 2026
…kflows-agui

# Conflicts:
#	src/frontend/src/stores/flowStore.ts
The v2 public endpoint only ran validate_flow_for_current_settings and
skipped validate_public_flow_no_code_execution, which the v1
build_public_tmp path applies. A public flow containing a Python
interpreter/REPL (or the legacy Python Code Structured tool, Smart
Transform lambda) was therefore an unauthenticated server-side
code-execution primitive (report H1-3754930).

Mirror v1: import the validator and call it right after the
public-access gate. PublicFlowValidationError subclasses
CustomComponentValidationError, so the existing handler already
sanitizes it to a 400 'This flow cannot be executed.' without leaking
the blocked component class names.

Add a non-mocking test that builds a public flow with a real
PythonREPLComponent and asserts the sanitized 400 (verified RED: returns
200 without the gate).

LE-1389
…rtex builds

A completed background job's GET status 500'd with 'No vertex builds found
for job_id'. The background build path differed from the sync path twice:

1. generate_flow_events minted a fresh run_id instead of using job_id, so
   vertex builds were keyed by an id the status query never uses. Thread
   run_id through _stream_event_frames -> generate_flow_events and pass
   job_id from the background buffer so graph.run_id == job_id (the sync
   path already does graph.set_run_id(job_id)).

2. The SSE build loop (build_vertices) only persisted builds when log_builds
   was set and never passed job_id. Tie log_builds to job-tracked runs
   (run_id present) and pass job_id=graph.run_id on the persist call.
   Job-tracked runs also persist streaming terminal vertices so
   reconstruction is complete; the live build path (run_id is None) keeps
   its original behavior, so the v1 build path is unchanged.

Test: a real background run polled to completion, then GET status asserts a
reconstructed 200 (verified RED: 500 'No vertex builds found' before the
fix). Covers the non-streaming flow. v1 build path unchanged (35 build
tests pass); AG-UI suite 46 pass.

LE-1389
Adopt 1.11.0's composed group-mixin Settings for lfx settings/base.py (all v2 settings already present in the new groups); port the Python 3.14 cors_origins ['*']->'*' fix into groups/security.py.
…ows-bg-default

Port the 6 background-execution settings and background_backend_is_scaled into 1.11.0's RuntimeSettings group mixin; union the BACKGROUND_EXECUTION_SERVICE / TELEMETRY_WRITER_SERVICE additions in schema.py and deps.py.
@github-actions github-actions Bot removed the enhancement New feature or request label Jun 10, 2026
@github-actions

This comment has been minimized.

1 similar comment
@github-actions

This comment has been minimized.

# Conflicts:
#	src/lfx/src/lfx/services/settings/groups/runtime.py
@github-actions

This comment has been minimized.

release-1.13.0 added d8f2c3a4b5e6 under the same parent, so the merge
left two alembic heads even though git merged cleanly. The index
migration now revises it under a fresh revision id (a9e4c1b7d306):
reusing an id across a re-parent lets a database already stamped with it
skip the migrations in between.
@github-actions

This comment has been minimized.

# Conflicts:
#	src/backend/base/langflow/services/background_execution/service.py
#	src/backend/base/langflow/services/jobs/service.py
#	src/backend/tests/unit/background_execution/test_real_services_harness.py
#	src/lfx/src/lfx/services/settings/groups/runtime.py
#	src/lfx/tests/unit/services/settings/test_settings_composition.py
Retention landed on the release branch in #15316 and is documented on the
environment variables page, but the background settings table on the
multi-worker page did not mention it. With the scaled backend the job
table is the work queue, so every finished run sits under the claim scan
until something deletes it, which is worth saying where an operator is
already reading about that queue.

The wording follows the shipped behavior: the window covers every
finished run in the job store, not only background runs.
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

release-1.13.0 added the flow version-token migrations and three merge
nodes, leaving our index migration's parent with a sibling, so the merge
produced two alembic heads even though the content merged cleanly. The
index migration now revises 4e7a2b9c1d05 under a fresh revision id
(b6f3a8d2c471): reusing an id across a re-parent lets a database already
stamped with it skip the migrations in between.
@github-actions

This comment has been minimized.

# Conflicts:
#	src/backend/base/langflow/services/background_execution/service.py
#	src/backend/base/langflow/services/database/models/jobs/model.py
#	src/backend/base/langflow/services/jobs/service.py
#	src/backend/tests/unit/services/background_execution/test_standalone_slice.py
#	src/lfx/tests/unit/services/settings/test_settings_composition.py
@github-actions

This comment has been minimized.

The extensionless ../Develop/environment-variables was resolved as a route
(/Develop/environment-variables), which does not exist because the doc routes
are flat. Docusaurus failed the build on it. The two other Deployment pages
that link this reference use the .mdx file path, which Docusaurus resolves
against the filesystem instead.
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@Jkavia

Jkavia commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator

Looked at the scaled backend against the usual db-as-a-queue failure modes. Core lease/claim/watchdog looks solid. A few things:

Heartbeat and terminal write. The heartbeat is an asyncio task on the same loop as the run (_beat in runner.py), so a component that blocks the loop stops the beat and the lease goes stale while the job is still running. Another worker's watchdog then marks the job failed, with reason worker_lost. The success path writes COMPLETED unconditionally, update_job_status -> update(Job).where(Job.job_id == job_id) at service.py:1557, with no status or owner guard, while fail_in_progress_job is guarded on status=IN_PROGRESS. So the blocked run finishes and flips the row from FAILED back to COMPLETED, leaving a completed row that still carries a run_failed event, and any client that reattached in between was already told it failed. Guarding the success write on status=IN_PROGRESS AND owner=me would close it.

Clock skew. is_lease_stale compares now() on the reconciler against the worker's heartbeat_at. Across multiple hosts that depends on NTP with no skew guard.

Retry backoff. A requeued retry_safe job flips straight to QUEUED and is immediately re-claimable, so a deterministic failure retries back-to-back to max_attempts. Default workflows don't set retry_safe so it's narrow, but a small delay would help.

A few that look deliberate, want to confirm:

  • One shared FIFO queue by created_timestamp, no fairness, so one caller flooding holds up everyone.
  • No depth cap on the scaled queue, submit just persists a QUEUED row so it grows until retention or disk.
  • No priority or isolation, so a slow flow class blocks faster ones behind it.

Minor: no single alembic head gate here, worth running alembic heads on the merge. And I haven't checked whether the started/completed counters double-count a retried job.

The watchdog's failure write was conditional but the success write was not, so
the two could not both be right. A run whose heartbeat stalled long enough to be
reconciled as worker_lost would finish and flip the row back from FAILED to
COMPLETED, leaving a completed job still carrying a run_failed event after a
reattached client had already been told it failed. The heartbeat is a task on
the run's own event loop, so any component that blocks the loop reaches this.

The success write is now conditional like the failure one. Guarded on the row
not already being terminal rather than on IN_PROGRESS, because several callers
pass mark_in_progress=False and own the status themselves; an IN_PROGRESS guard
would strand their jobs non-terminal instead.

The owner guard covers the requeue case, where the row is IN_PROGRESS again
under somebody else and a status check alone cannot tell the difference. An
absent owner passes it: the owner is written by the heartbeat rather than at
claim time, so a run short enough to finish before its first beat has no name on
its row and must still complete. The guard is 'not somebody else's', not
'provably mine'.

Losing the write now logs rather than passing silently, since the race firing is
worth seeing.
# Conflicts:
#	docs/docs/Support/troubleshooting.mdx
@github-actions

This comment has been minimized.

@ogabrielluiz

Copy link
Copy Markdown
Contributor Author

Thanks, the terminal write is a real bug and you're right about the cause.

Fixed in 38a3ac9. Two deviations from your suggestion worth flagging:

Guarded on "not already terminal" rather than IN_PROGRESS. Several callers pass mark_in_progress=False and own the status themselves, so an IN_PROGRESS guard would have stranded their jobs non-terminal instead.

The owner guard is in, but an absent owner has to pass it. Owner is written by the heartbeat rather than at claim time, so a run that finishes before its first beat has no name on its row. My first version required a match and stranded exactly the fastest jobs. It checks "not somebody else's" rather than "provably mine". Losing the write logs a warning now instead of passing silently.

On the counters you hadn't checked: no double-count. retry_requeue_claim updates the same row and the counters count rows, so a retry counts once. The flip side is that retries are invisible, a job that failed twice then succeeded reads started=1, completed=1, failed=0. That probably wants its own metric.

Clock skew and retry backoff are both real and unfixed. Skew is inherent to comparing the reconciler's clock against a worker's timestamp, and I don't have better than requiring NTP and saying so. Backoff I'd like to do, it's small.

The three you asked about are deliberate, and they're in the description as the known ceiling with SKIP LOCKED as the marked upgrade path. The depth cap is the one I'd reconsider soonest, since growing until retention or disk is a worse failure than a rejected submit. Do you think that should come now rather than later?

alembic heads is single on the merge, c7d2b9e4f813. Agree it should be gated rather than left to habit.

release-1.14.0 brings a migration of its own, so the branch had two alembic
heads after the merge. The index now follows 1d8d193028c8 and carries a fresh
revision id, because reusing the old one lets a database already stamped with it
skip the migration entirely.
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Build successful! ✅
Deploying docs draft.
Deploy successful! View draft

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request scaling-up

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants