<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/escaping-the-n-1-trap-how-i-fixed-my-fastapi-app-s-silent-performance-killer-puhpco2tq" -->

---
title: Escaping the N+1 Trap: How I Fixed My FastAPI App’s...
description: A developer building a FastAPI Library Management System with SQLAlchemy async discovered a classic N+1 query problem causing 1450ms response times. By...
canonical: https://daily.dev/posts/escaping-the-n-1-trap-how-i-fixed-my-fastapi-app-s-silent-performance-killer-puhpco2tq
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: Escaping the N+1 Trap: How I Fixed My FastAPI App’s Silent Performance Killer | daily.dev
og:description: A developer building a FastAPI Library Management System with SQLAlchemy async discovered a classic N+1 query problem causing 1450ms response times. By...
og:url: https://daily.dev/posts/escaping-the-n-1-trap-how-i-fixed-my-fastapi-app-s-silent-performance-killer-puhpco2tq
og:image: https://api.daily.dev/og/posts/PuHPcO2tQ.png
og:image:alt: Escaping the N+1 Trap: How I Fixed My FastAPI App’s Silent Performance Killer
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Escaping the N+1 Trap: How I Fixed My FastAPI App’s Silent Performance Killer

**[Medium](https://daily.dev/sources/medium_js)** · 9 min read · 0 upvotes · 0 comments

## Summary

A developer building a FastAPI Library Management System with SQLAlchemy async discovered a classic N+1 query problem causing 1450ms response times. By instrumenting the app with OpenTelemetry and routing traces to SigNoz, the waterfall view revealed 51 sequential database spans triggered by lazy-loading ORM relationships in a loop. Switching to SQLAlchemy's `selectinload` eager loading strategy collapsed 51 spans into 2, cutting response time by 87% to 185ms. The post also covers a gotcha with OpenTelemetry context propagation across thread boundaries when using executor threads, requiring manual context attach/detach to avoid orphaned spans and memory leaks.

## Full article

daily.dev links to this article rather than hosting it. Read it at the original source: <https://medium.com/@ritammaji1611/escaping-the-n-1-trap-how-i-fixed-my-fastapi-apps-silent-performance-killer-2a70cb8bdbd2>

## Questions this post answers

### How do I fix an N+1 query problem in an async FastAPI app using SQLAlchemy?

Use SQLAlchemy's selectinload eager-loading strategy instead of accessing relationship attributes inside a loop. Wrapping the query with select(Author).options(selectinload(Author.books)) collapsed 51 sequential database spans into just 2 batched queries, dropping endpoint latency from 1450ms to 185ms, an 87% improvement, in a traced FastAPI and asyncpg setup.

_daily.dev surfaces practical fixes like this for developers hunting down slow ORM query patterns._

### Why do my OpenTelemetry traces show orphaned spans when work runs in a thread pool executor?

OpenTelemetry stores trace context in thread-local storage, so when execution moves from the async event loop into a separate thread via run_in_executor, that context is empty and the resulting spans appear disconnected. The fix is to capture context.get_current() before dispatching the work, call context.attach(ctx) inside the worker thread, and always context.detach(token) in a finally block to avoid memory leaks and cross-pollinated traces.

_developers debugging async-to-sync tracing gaps can find similar deep dives through daily.dev._

### What ports does the OpenTelemetry Collector need open to receive traces from an application?

Port 4317 is used for OTLP over gRPC and port 4318 for OTLP over HTTP, and both must be explicitly configured as receivers in the collector's config.yaml or telemetry data will be silently dropped without any error. Without these correctly set, traces exported from an app like a SigNoz-connected FastAPI service simply never arrive.

_daily.dev helps developers wiring up observability pipelines avoid this kind of silent misconfiguration._

## Similar posts on daily.dev

- [We Fixed N\+1 With Eager Loading. Memory Usage Tripled.](https://daily.dev/posts/we-fixed-n-1-with-eager-loading-memory-usage-tripled--4oqpykzth) · Medium · 54 upvotes · 7 comments

---

Tags: [#python](https://daily.dev/tags/python), [#opentelemetry](https://daily.dev/tags/opentelemetry), [#fastapi](https://daily.dev/tags/fastapi)

[View this post on daily.dev](https://daily.dev/posts/escaping-the-n-1-trap-how-i-fixed-my-fastapi-app-s-silent-performance-killer-puhpco2tq)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"TechArticle","headline":"Escaping the N+1 Trap: How I Fixed My FastAPI App’s Silent Performance Killer","url":"https://daily.dev/posts/escaping-the-n-1-trap-how-i-fixed-my-fastapi-app-s-silent-performance-killer-puhpco2tq","mainEntityOfPage":{"@type":"WebPage","@id":"https://daily.dev/posts/escaping-the-n-1-trap-how-i-fixed-my-fastapi-app-s-silent-performance-killer-puhpco2tq"},"datePublished":"2026-07-22T18:11:16.648Z","dateModified":"2026-09-14T06:25:43.100Z","description":"A developer building a FastAPI Library Management System with SQLAlchemy async discovered a classic N+1 query problem causing 1450ms response times. By...","image":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/d6ab0a9a223ee01bc82d647f54feb2d2?_a=AQAEuop","thumbnailUrl":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/d6ab0a9a223ee01bc82d647f54feb2d2?_a=AQAEuop","isAccessibleForFree":true,"articleSection":"Medium","inLanguage":"en","publisher":{"@type":"Organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180}},"author":{"@type":"Organization","name":"Medium","logo":"https://media.daily.dev/image/upload/t_logo,f_auto/v1/logos/medium","url":"https://daily.dev/sources/medium_js"},"commentCount":0,"discussionUrl":"https://daily.dev/posts/escaping-the-n-1-trap-how-i-fixed-my-fastapi-app-s-silent-performance-killer-puhpco2tq","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":0},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":0}],"keywords":"python,opentelemetry,fastapi","timeRequired":"PT9M"}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"Medium","item":"https://daily.dev/sources/medium_js"},{"@type":"ListItem","position":3,"name":"Escaping the N+1 Trap: How I Fixed My FastAPI App’s Silent Performance Killer"}]}
{"@context":"https://schema.org","@type":"FAQPage","@id":"https://daily.dev/posts/escaping-the-n-1-trap-how-i-fixed-my-fastapi-app-s-silent-performance-killer-puhpco2tq#faq","mainEntity":[{"@type":"Question","name":"How do I fix an N+1 query problem in an async FastAPI app using SQLAlchemy?","acceptedAnswer":{"@type":"Answer","text":"Use SQLAlchemy's selectinload eager-loading strategy instead of accessing relationship attributes inside a loop. Wrapping the query with select(Author).options(selectinload(Author.books)) collapsed 51 sequential database spans into just 2 batched queries, dropping endpoint latency from 1450ms to 185ms, an 87% improvement, in a traced FastAPI and asyncpg setup. daily.dev surfaces practical fixes like this for developers hunting down slow ORM query patterns."}},{"@type":"Question","name":"Why do my OpenTelemetry traces show orphaned spans when work runs in a thread pool executor?","acceptedAnswer":{"@type":"Answer","text":"OpenTelemetry stores trace context in thread-local storage, so when execution moves from the async event loop into a separate thread via run_in_executor, that context is empty and the resulting spans appear disconnected. The fix is to capture context.get_current() before dispatching the work, call context.attach(ctx) inside the worker thread, and always context.detach(token) in a finally block to avoid memory leaks and cross-pollinated traces. developers debugging async-to-sync tracing gaps can find similar deep dives through daily.dev."}},{"@type":"Question","name":"What ports does the OpenTelemetry Collector need open to receive traces from an application?","acceptedAnswer":{"@type":"Answer","text":"Port 4317 is used for OTLP over gRPC and port 4318 for OTLP over HTTP, and both must be explicitly configured as receivers in the collector's config.yaml or telemetry data will be silently dropped without any error. Without these correctly set, traces exported from an app like a SigNoz-connected FastAPI service simply never arrive. daily.dev helps developers wiring up observability pipelines avoid this kind of silent misconfiguration."}}]}
```

