Skip to content

perf: enable Gunicorn preload_app to reduce memory per worker#12364

Merged
jordanrfrazier merged 12 commits into
langflow-ai:release-1.9.0from
severfire:preload_app_optimization
Apr 9, 2026
Merged

perf: enable Gunicorn preload_app to reduce memory per worker#12364
jordanrfrazier merged 12 commits into
langflow-ai:release-1.9.0from
severfire:preload_app_optimization

Conversation

@severfire
Copy link
Copy Markdown
Contributor

Description: Adds "preload_app": True to the Gunicorn options for non-Windows environments.

Why?
Previously, each Gunicorn worker spawned a completely independent Python interpreter, duplicating the entire module and import footprint in RAM. By preloading the app in the master process before forking, the OS (Linux/macOS) can use Copy-on-Write (CoW) to share read-only memory—such as Python bytecode, class definitions, and import-time structures—across all worker processes.

Impact & Safety:
Lower Memory Usage: Significantly reduces the baseline RAM required for each additional worker.
Faster Worker Boot: Workers start up faster since the app imports are already resolved.
Connection Safe: Because Langflow cleanly initializes its stateful connections (Database pools, Telemetry clients, MCP services) inside the async lifespan context manager, connections are safely created post-fork in each worker's dedicated event loop. No sockets or file descriptors are incorrectly shared.

@github-actions github-actions Bot added community Pull Request from an external contributor performance Maintenance tasks and housekeeping labels Mar 27, 2026
@github-actions github-actions Bot added performance Maintenance tasks and housekeeping and removed performance Maintenance tasks and housekeeping labels Mar 27, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 27, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 13e77e5b-6e67-433b-a33a-4242a673e0fd

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Enables Gunicorn app preloading to reduce per-worker memory usage via Copy-on-Write sharing after fork.

Changes:

  • Adds preload_app: True to the Gunicorn options passed to LangflowApplication.
  • Updates the generated component index asset, including dependency version entries and the recorded sha256.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/backend/base/langflow/__main__.py Enables Gunicorn preload_app to preload the app in the master process before forking workers.
src/lfx/src/lfx/_assets/component_index.json Updates generated component index metadata (dependency versions + sha256).
Comments suppressed due to low confidence (2)

src/lfx/src/lfx/_assets/component_index.json:1

  • This PR is described as enabling Gunicorn preload_app, but it also includes changes to the generated component index (e.g., google dependency version entries and the asset sha256). If this file is expected to change, please update the PR description to mention it and why; otherwise, consider reverting this file (or moving it into a separate PR) to keep the change set focused and easier to review.
    src/lfx/src/lfx/_assets/component_index.json:1
  • This PR is described as enabling Gunicorn preload_app, but it also includes changes to the generated component index (e.g., google dependency version entries and the asset sha256). If this file is expected to change, please update the PR description to mention it and why; otherwise, consider reverting this file (or moving it into a separate PR) to keep the change set focused and easier to review.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/backend/base/langflow/__main__.py Outdated
"certfile": ssl_cert_file_path,
"keyfile": ssl_key_file_path,
"log_level": log_level.lower() if log_level is not None else "info",
"preload_app": True,
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enabling preload_app can change runtime behavior because all import-time side effects run in the master process and are then forked into workers. To reduce operational risk and ease debugging/rollbacks, consider making this configurable (e.g., env var/CLI flag defaulting to enabled on supported platforms) and documenting any fork-safety assumptions (no sockets/FDs or background threads created at import time).

Copilot uses AI. Check for mistakes.
@severfire
Copy link
Copy Markdown
Contributor Author

@ogabrielluiz thank you for the earlier! seems like this is much easier! hope this one looks good as well :-)

@github-actions github-actions Bot added performance Maintenance tasks and housekeeping and removed performance Maintenance tasks and housekeeping labels Apr 8, 2026
@github-actions github-actions Bot added performance Maintenance tasks and housekeeping and removed performance Maintenance tasks and housekeeping labels Apr 8, 2026
@severfire
Copy link
Copy Markdown
Contributor Author

@jordanrfrazier please let us know, would love to get your approval :-D thank you!

@jordanrfrazier
Copy link
Copy Markdown
Collaborator

@severfire Thanks for the find, this looks very promising. I just clicked around quick and saw some articles on this flag, and I think we'll want to spend some time testing this flag before making it default behavior.

In particular, I'm worried we'll run into the same "ghost" process issue detailed in this article.
https://www.rippling.com/blog/rippling-gunicorn-pre-fork-journey-memory-savings-and-cost-reduction

So here's what I can do now - I'm going to make this a configurable environment variable that defaults to the existing behavior, but will give you and others the ability to toggle it on and see how it works. If all goes well, I'll make this the default setting for v1.10.

@severfire
Copy link
Copy Markdown
Contributor Author

@jordanrfrazier configurable environment variable that defaults to the existing behavior sound great!

@github-actions github-actions Bot added performance Maintenance tasks and housekeeping and removed performance Maintenance tasks and housekeeping labels Apr 8, 2026
@github-actions github-actions Bot added performance Maintenance tasks and housekeeping and removed performance Maintenance tasks and housekeeping labels Apr 8, 2026
@github-actions github-actions Bot added performance Maintenance tasks and housekeeping and removed performance Maintenance tasks and housekeeping labels Apr 8, 2026
@jordanrfrazier jordanrfrazier enabled auto-merge April 8, 2026 16:31
@github-actions github-actions Bot added performance Maintenance tasks and housekeeping and removed performance Maintenance tasks and housekeeping labels Apr 8, 2026
@severfire
Copy link
Copy Markdown
Contributor Author

@jordanrfrazier - i will work on potential ghosts. Opus 4.6 gave me possible solutions


Dealing with "Ghosts" when preload_app: True

The Rippling blog describes three types of "ghosts" that survive a fork(): TCP connections, threads, and file descriptors created in the master process that become dead/corrupt in child workers. Here's how this applies to Langflow and what can be done.

Langflow's Current Architecture

Langflow already uses preload_app: True (line 416 of __main__.py). The flow is:

  1. Master process runs setup_app()create_app() (builds the FastAPI app object)
  2. Master forks child workers
  3. Each worker (a LangflowUvicornWorker) creates its own event loop and triggers the ASGI lifespan, which is where the heavy initialization happens (initialize_services, DB engine creation, Redis, MCP, etc.)

This is actually a reasonably safe architecture because Langflow uses async UvicornWorkers, so the ASGI lifespan runs per-worker. But there are still concrete ghosts hiding in create_app():

Current Ghosts in create_app() (master process)

1. Prometheus HTTP server — the most obvious ghost:

    if settings.prometheus_enabled:
        from prometheus_client import start_http_server

        start_http_server(settings.prometheus_port)

This creates a TCP listener socket in the master. After fork, every worker inherits a reference to this socket — a classic ghost that will cause port conflicts or corrupted metrics.

2. Sentry SDK initialization:

def setup_sentry(app: FastAPI) -> None:
    settings = get_settings_service().settings
    if settings.sentry_dsn:
        import sentry_sdk
        from sentry_sdk.integrations.asgi import SentryAsgiMiddleware

        sentry_sdk.init(
            dsn=settings.sentry_dsn,
            traces_sample_rate=settings.sentry_traces_sample_rate,
            profiles_sample_rate=settings.sentry_profiles_sample_rate,
        )
        app.add_middleware(SentryAsgiMiddleware)

sentry_sdk.init() may create a background thread for its transport layer and open HTTP connections to the Sentry ingest endpoint. Both are ghosts after fork.

3. OpenTelemetry instrumentation:

    FastAPIInstrumentor.instrument_app(app)

Depending on the configured exporter, OTEL may create background threads and HTTP connections.

4. PostgreSQL pre-flight check in the launcher:

            try:
                check_postgresql_version_sync(database_url)
            except UnsupportedPostgreSQLVersionError:
                sys.exit(1)

This creates a synchronous SQLAlchemy engine + connection. The engine should be garbage collected after the function returns, but if any connection pool lingers, it's a ghost.

The Three-Step Rippling Pattern Applied to Langflow

Step 1: Detect ghosts (monkey-patching audit)

Rippling's approach of monkey-patching socket.socket.connect and threading._newname to dump stack traces would work here. You'd add a diagnostic mode that dumps every TCP connection and thread created during create_app():

import socket
import threading
import traceback

_original_connect = socket.socket.connect
def _audit_connect(self, address):
    traceback.print_stack()
    print(f"TCP connection to {address}")
    return _original_connect(self, address)

socket.socket.connect = _audit_connect

Run this once, collect the hit list, then remove it.

Step 2: Disconnect/reconnect hooks via Gunicorn lifecycle

The LangflowApplication class in server.py is a Gunicorn BaseApplication. You can add Gunicorn's built-in post_fork and pre_fork hooks:

class LangflowApplication(BaseApplication):
    def __init__(self, app, options=None) -> None:
        self.options = options or {}
        self.options["worker_class"] = "langflow.server.LangflowUvicornWorker"
        self.options["logger_class"] = Logger
        self.application = app
        super().__init__()

    def load_config(self) -> None:
        config = {
            key: value
            for key, value in self.options.items()
            if key in self.cfg.settings and value is not None
        }
        for key, value in config.items():
            self.cfg.set(key.lower(), value)

        # Register lifecycle hooks
        self.cfg.set("post_fork", self.post_fork)

    def load(self):
        return self.application

    @staticmethod
    def post_fork(server, worker):
        """Called in each worker after fork. Reinitialize connections here."""
        import gc

        # Force GC to clean up any inherited dead objects
        gc.collect()

        # Re-init Sentry transport (if used)
        # sentry_sdk's transport thread is dead after fork
        try:
            import sentry_sdk
            client = sentry_sdk.get_client()
            if client and client.transport:
                client.transport = type(client.transport)(client.options)
        except Exception:
            pass

Step 3: Move ghost-producing code out of create_app()

The cleanest fix for each ghost:

  • Prometheus: Move start_http_server into the ASGI lifespan (runs per-worker), or use a multiprocess-aware Prometheus setup (prometheus_client.values.MultiProcessValue).

  • Sentry: Initialize with transport=None in master, then call sentry_sdk.init() again in post_fork or in the ASGI lifespan.

  • OpenTelemetry: Defer exporter initialization to the lifespan.

  • PostgreSQL pre-flight: This already runs before LangflowApplication is created and likely cleans up, but you can explicitly engine.dispose() after the check.

The Guardrail (Fail-Fast Check)

Rippling's most valuable contribution is the pre-fork guardrail — a function that crashes the process if any stray connections or threads exist right before fork(). For Langflow, you'd add this as a pre_fork hook:

@staticmethod
def pre_fork(server, worker):
    """Runs in master just before fork. Crash if ghosts found."""
    import psutil
    import threading

    tcp_conns = psutil.Process().net_connections(kind="tcp")
    if tcp_conns:
        details = [(c.laddr, c.raddr, c.status) for c in tcp_conns]
        raise RuntimeError(
            f"Ghost TCP connections found before fork: {details}"
        )

    non_main_threads = [
        t for t in threading.enumerate()
        if t.is_alive() and t is not threading.main_thread()
    ]
    if non_main_threads:
        raise RuntimeError(
            f"Ghost threads found before fork: {[t.name for t in non_main_threads]}"
        )

Bonus: gc.freeze() for CoW Optimization

As the blog mentions, calling gc.collect() + gc.freeze() right before fork prevents the GC from scanning shared memory pages in workers, maximizing Copy-on-Write savings:

@staticmethod
def pre_fork(server, worker):
    import gc
    gc.collect()
    gc.freeze()

Summary

Ghost Source Runs in Master? Fix
Prometheus start_http_server Yes Move to lifespan or use multiprocess mode
sentry_sdk.init() Yes Defer to post_fork or lifespan
OTEL instrumentation Yes (partial) Defer exporter init to lifespan
PG version check Yes (before app) Explicitly engine.dispose()
DB engine, Redis, services No (lifespan) Already safe

Langflow is in a better position than Rippling's Django monolith because the ASGI lifespan pattern already defers heavy initialization to each worker. The main action items are the Prometheus server, Sentry SDK, and adding the fail-fast guardrail to prevent regressions.

@github-actions github-actions Bot added performance Maintenance tasks and housekeeping and removed performance Maintenance tasks and housekeeping labels Apr 8, 2026
@jordanrfrazier
Copy link
Copy Markdown
Collaborator

@severfire Awesome. I'll get the current configurable state in for v1.9, and have created an internal task tracking the follow up for v1.10. If you have any findings on whether it will be an issue or not, please open a PR or issue and tag me.

@severfire
Copy link
Copy Markdown
Contributor Author

@jordanrfrazier Sounds great! Thank you! I will create new branch based on it and work on ghosts there.

@Adam-Aghili
Copy link
Copy Markdown
Collaborator

merge conflicts

@github-actions github-actions Bot added performance Maintenance tasks and housekeeping and removed performance Maintenance tasks and housekeeping labels Apr 9, 2026
@github-actions github-actions Bot added performance Maintenance tasks and housekeeping and removed performance Maintenance tasks and housekeeping labels Apr 9, 2026
@jordanrfrazier jordanrfrazier disabled auto-merge April 9, 2026 01:24
@jordanrfrazier jordanrfrazier merged commit afbc6b0 into langflow-ai:release-1.9.0 Apr 9, 2026
181 of 187 checks passed
@severfire
Copy link
Copy Markdown
Contributor Author

@jordanrfrazier Hi, was this released in https://github.com/langflow-ai/langflow/releases/tag/v1.9.0 ? I do not see it on the list

@jordanrfrazier
Copy link
Copy Markdown
Collaborator

jordanrfrazier commented Apr 15, 2026

@severfire It was, yes. Not sure why it's not shown on the release. Will check.

update: yeah. new release process, generated logs from last release. I'll get the team to fix it up

@severfire
Copy link
Copy Markdown
Contributor Author

@jordanrfrazier thanks! waiting for the update then :-) I made some fixes with ghosting #12587, and also other issue i found with job queue #12588

Adam-Aghili pushed a commit that referenced this pull request Apr 15, 2026
* fix: enable preload_app option in LangflowApplication configuration

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* Make flag configurable

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes (attempt 3/3)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@logspace.ai>
Co-authored-by: ogabrielluiz <gabriel@langflow.org>
Co-authored-by: Jordan Frazier <jordan.frazier@datastax.com>
Co-authored-by: Jordan Frazier <122494242+jordanrfrazier@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community Pull Request from an external contributor performance Maintenance tasks and housekeeping

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants