Skip to content

⚡ Bolt: Offload blocking I/O calls in FastAPI endpoints to worker threads#273

Open
anchapin wants to merge 1 commit intomainfrom
bolt-fastapi-async-offload-2772379728622250420
Open

⚡ Bolt: Offload blocking I/O calls in FastAPI endpoints to worker threads#273
anchapin wants to merge 1 commit intomainfrom
bolt-fastapi-async-offload-2772379728622250420

Conversation

@anchapin
Copy link
Copy Markdown
Owner

@anchapin anchapin commented Apr 28, 2026

💡 What: Wrapped blocking operations like generator.generate, pdf_path.read_bytes, and resume_yaml_path.write_text in FastAPI async endpoints using anyio.to_thread.run_sync combined with functools.partial.
🎯 Why: Synchronous operations inside an async def FastAPI route block the ASGI event loop, preventing concurrent handling of incoming HTTP requests and causing severe performance degradation under load.
📊 Impact: Eliminates event loop blocking during heavy operations like PDF rendering and AI tailing. Expected to drastically improve throughput and reduce latency spikes for concurrent API requests.
🔬 Measurement: Run a load test against the API with parallel requests (e.g., using wrk or Locust). Verify that response times for fast endpoints remain low even while slow endpoints (like PDF rendering) are running concurrently.


PR created automatically by Jules for task 2772379728622250420 started by @anchapin

Summary by Sourcery

Offload blocking resume and cover letter generation work in FastAPI endpoints to background worker threads to avoid blocking the event loop and improve concurrency.

Enhancements:

  • Wrap synchronous generator calls and heavy file I/O in key FastAPI endpoints (PDF rendering, tailoring, ATS check, cover letter generation, and resume PDF rendering) with anyio.to_thread.run_sync to run them in worker threads.

Documentation:

  • Update the internal Bolt notes to document the pattern of offloading blocking operations in FastAPI routes using anyio.to_thread.run_sync and functools.partial.

- Wraps blocking function calls (e.g., generator.generate, file read/write) with `anyio.to_thread.run_sync` inside FastAPI async routes.
- Resolves event loop starvation caused by CPU/IO heavy blocking operations.

Co-authored-by: anchapin <[email protected]>
@google-labs-jules
Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 28, 2026

Reviewer's Guide

Offloads blocking file and generator operations in FastAPI async endpoints to worker threads using anyio.to_thread.run_sync with functools.partial, and updates internal Bolt documentation to capture this async I/O offloading pattern.

Sequence diagram for FastAPI route offloading blocking I/O to worker threads

sequenceDiagram
    actor Client
    participant FastAPIApp
    participant AnyioThreadWorker
    participant Generator
    participant FileSystem

    Client->>FastAPIApp: HTTP POST render_pdf
    activate FastAPIApp

    FastAPIApp->>AnyioThreadWorker: anyio.to_thread.run_sync(generator.generate)
    deactivate FastAPIApp
    activate AnyioThreadWorker
    AnyioThreadWorker->>Generator: generate(variant, output_format, output_path)
    Generator->>FileSystem: write PDF to output_path
    FileSystem-->>Generator: PDF written
    Generator-->>AnyioThreadWorker: return
    deactivate AnyioThreadWorker
    activate FastAPIApp

    FastAPIApp->>AnyioThreadWorker: anyio.to_thread.run_sync(output_pdf.exists)
    deactivate FastAPIApp
    activate AnyioThreadWorker
    AnyioThreadWorker->>FileSystem: check file exists
    FileSystem-->>AnyioThreadWorker: exists bool
    AnyioThreadWorker-->>FastAPIApp: exists bool
    deactivate AnyioThreadWorker
    activate FastAPIApp

    FastAPIApp->>AnyioThreadWorker: anyio.to_thread.run_sync(output_pdf.read_bytes)
    deactivate FastAPIApp
    activate AnyioThreadWorker
    AnyioThreadWorker->>FileSystem: read_bytes
    FileSystem-->>AnyioThreadWorker: PDF bytes
    AnyioThreadWorker-->>FastAPIApp: PDF bytes
    deactivate AnyioThreadWorker
    activate FastAPIApp

    FastAPIApp-->>Client: HTTP 200 PDF response
    deactivate FastAPIApp
Loading

Flow diagram for using anyio.to_thread.run_sync with functools.partial

flowchart TD
    A[Start async FastAPI route] --> B[Identify blocking sync function call]
    B --> C[Prepare callable using functools.partial]
    C --> D[Call anyio.to_thread.run_sync with partial]
    D --> E[Blocking work runs in worker thread]
    E --> F[Worker thread performs file I O or generator call]
    F --> G[Result returned to event loop]
    G --> H[Async route awaits result without blocking event loop]
    H --> I[Compose HTTP response]
    I --> J[Return response to client]
Loading

File-Level Changes

Change Details Files
Offload synchronous PDF generation and file I/O in render_pdf endpoint to worker threads to avoid blocking the event loop.
  • Wrap generator.generate call in anyio.to_thread.run_sync via functools.partial
  • Move output_pdf.exists check into anyio.to_thread.run_sync
  • Move output_pdf.read_bytes call into anyio.to_thread.run_sync to read PDF content asynchronously
api/main.py
Offload AI tailoring, ATS report generation, and cover letter generation work from async endpoints to worker threads.
  • Wrap AIGenerator.tailor_data call in anyio.to_thread.run_sync via functools.partial
  • Wrap ATSGenerator.generate_report call in anyio.to_thread.run_sync via functools.partial
  • Wrap generator.generate_non_interactive call in anyio.to_thread.run_sync via functools.partial
api/main.py
Offload cover letter PDF compilation and reading from async endpoint to worker threads.
  • Wrap generator._compile_pdf call in anyio.to_thread.run_sync via functools.partial before checking result
  • Replace manual file open/read with pdf_path.read_bytes wrapped in anyio.to_thread.run_sync when returning base64-encoded PDF
api/main.py
Offload resume YAML writing, PDF generation, and file I/O in render_resume_pdf endpoint to worker threads.
  • Generate YAML string in-memory with yaml.dump instead of writing via open
  • Use resume_yaml_path.write_text wrapped in anyio.to_thread.run_sync via functools.partial to write YAML to disk
  • Wrap TemplateGenerator.generate call in anyio.to_thread.run_sync via functools.partial
  • Move output_pdf.exists check into anyio.to_thread.run_sync
  • Move output_pdf.read_bytes call into anyio.to_thread.run_sync to read generated resume PDF
api/main.py
Update Bolt documentation note to describe the async I/O offloading pattern for FastAPI routes.
  • Replace previous performance tips with a new note about using anyio.to_thread.run_sync and functools.partial for blocking operations in FastAPI async routes
.jules/bolt.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In render_resume_pdf, yaml.dump is still executed on the event loop and can be relatively heavy; consider offloading the yaml.dump call itself into anyio.to_thread.run_sync rather than just the write_text to fully avoid blocking.
  • The same anyio.to_thread.run_sync(functools.partial(...)) pattern is repeated in several endpoints; consider introducing a small helper utility (e.g., run_in_thread(func, **kwargs)) to centralize this and reduce duplication.
  • The update to .jules/bolt.md replaces previous performance notes rather than appending a new entry; double-check whether those historical notes are meant to be preserved alongside the new async I/O guidance.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `render_resume_pdf`, `yaml.dump` is still executed on the event loop and can be relatively heavy; consider offloading the `yaml.dump` call itself into `anyio.to_thread.run_sync` rather than just the `write_text` to fully avoid blocking.
- The same `anyio.to_thread.run_sync(functools.partial(...))` pattern is repeated in several endpoints; consider introducing a small helper utility (e.g., `run_in_thread(func, **kwargs)`) to centralize this and reduce duplication.
- The update to `.jules/bolt.md` replaces previous performance notes rather than appending a new entry; double-check whether those historical notes are meant to be preserved alongside the new async I/O guidance.

## Individual Comments

### Comment 1
<location path="api/main.py" line_range="572-578" />
<code_context>

-        with open(resume_yaml_path, "w", encoding="utf-8") as f:
-            yaml.dump(yaml_data, f, default_flow_style=False)
+        yaml_str = yaml.dump(yaml_data, default_flow_style=False)
+        await anyio.to_thread.run_sync(
+            functools.partial(resume_yaml_path.write_text, yaml_str, encoding="utf-8")
</code_context>
<issue_to_address>
**suggestion (performance):** `yaml.dump` itself may be the heaviest part of this operation and is still running on the event loop thread.

The file write is now offloaded, but YAML serialization for larger resumes can still block the event loop. If you expect large payloads or high concurrency, consider running `yaml.dump` inside `to_thread.run_sync` as well (e.g., perform both dump and write in the same worker-thread function).

```suggestion
        converter = JSONResumeConverter()
        yaml_data = converter.json_resume_to_yaml(_resume_storage[resume_id]["json_resume"])

        await anyio.to_thread.run_sync(
            lambda: resume_yaml_path.write_text(
                yaml.dump(yaml_data, default_flow_style=False),
                encoding="utf-8",
            )
        )
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread api/main.py
Comment on lines 572 to +578
converter = JSONResumeConverter()
yaml_data = converter.json_resume_to_yaml(_resume_storage[resume_id]["json_resume"])

with open(resume_yaml_path, "w", encoding="utf-8") as f:
yaml.dump(yaml_data, f, default_flow_style=False)
yaml_str = yaml.dump(yaml_data, default_flow_style=False)
await anyio.to_thread.run_sync(
functools.partial(resume_yaml_path.write_text, yaml_str, encoding="utf-8")
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (performance): yaml.dump itself may be the heaviest part of this operation and is still running on the event loop thread.

The file write is now offloaded, but YAML serialization for larger resumes can still block the event loop. If you expect large payloads or high concurrency, consider running yaml.dump inside to_thread.run_sync as well (e.g., perform both dump and write in the same worker-thread function).

Suggested change
converter = JSONResumeConverter()
yaml_data = converter.json_resume_to_yaml(_resume_storage[resume_id]["json_resume"])
with open(resume_yaml_path, "w", encoding="utf-8") as f:
yaml.dump(yaml_data, f, default_flow_style=False)
yaml_str = yaml.dump(yaml_data, default_flow_style=False)
await anyio.to_thread.run_sync(
functools.partial(resume_yaml_path.write_text, yaml_str, encoding="utf-8")
)
converter = JSONResumeConverter()
yaml_data = converter.json_resume_to_yaml(_resume_storage[resume_id]["json_resume"])
await anyio.to_thread.run_sync(
lambda: resume_yaml_path.write_text(
yaml.dump(yaml_data, default_flow_style=False),
encoding="utf-8",
)
)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant