بلاگز / FastAPI میں Blocked Event Loop: تشخیص اور حل

FastAPI میں Blocked Event Loop: تشخیص اور حل

شائع ہوا
26 اگست، 2026
مصنف
Faizan Nadeem
ٹیگز
FastAPI Python AsyncIO Performance
ایک گھڑی کا قریب سے لیا گیا منظر جس کے اندرونی گیئرز اور میکانزم نمایاں ہیں
Unsplash پر Peter Bryan کی تصویر

async def بنا دیں” وہ مشورہ ہے جو ہر FastAPI developer سب سے پہلے سنتا ہے، اور یہی وجہ ہے کہ production میں بہت سے incidents ایک سست endpoint سے شروع ہوتے ہیں جو خاموشی سے اپنے ساتھ ہر دوسرے endpoint کو بھی گرا دیتا ہے۔ یہ اندازہ بظاہر معقول لگتا ہے — async def “تیز” آپشن دکھائی دیتا ہے اور def پرانا — لیکن یہ فریم ورک اصل خرابی کو الٹا سمجھتا ہے۔ async def مفت concurrency نہیں دیتا۔ یہ event loop سے ایک وعدہ ہے کہ یہ function کبھی بیٹھ کر بلاک نہیں کرے گا، اور FastAPI کے پاس یہ جانچنے کا کوئی طریقہ نہیں کہ آپ نے یہ وعدہ نبھایا بھی ہے یا نہیں، جب تک بہت دیر نہ ہو جائے۔

اصل کہانی “ہر جگہ async استعمال کریں” سے کہیں زیادہ مخصوص ہے، اور اسے سمجھ لینا اس API کے درمیان فرق ہے جو load کے نیچے بھی باوقار انداز میں degrade کرتی ہے، اور اس API کے درمیان جہاں async def handler کے اندر صرف ایک requests.get() کال server پر موجود ہر concurrent user کو جما دیتی ہے، صرف اس user کو نہیں جس نے وہ request کی تھی۔ اس پوسٹ میں ہم دیکھیں گے کہ جب coroutine بلاک ہوتی ہے تو حقیقت میں کیا ہوتا ہے، def routes بالکل مختلف طرح کیوں برتاؤ کرتے ہیں، اس blocking call کو کیسے پکڑا جائے جو کسی ایسی library میں چھپی ہو جس کا آپ نے audit نہ کیا ہو، اور اسے ڈھونڈ لینے کے بعد حقیقت میں کیا کرنا چاہیے۔

آپ یہ سیکھیں گے:

  • FastAPI کا event loop اصل میں کیا ہے، اور یہ ایک وقت میں صرف ایک ہی کام کیوں کر سکتا ہے
  • async def اور def routes کے درمیان حقیقی فرق کیا ہے، اور FastAPI اندرونی طور پر ان کے ساتھ مکمل طور پر مختلف برتاؤ کیوں کرتا ہے
  • def routes جس threadpool میں چلتے ہیں وہ event loop کو کیسے محفوظ رکھتی ہے، اور یہی حفاظتی جال async def کے لیے کیوں موجود نہیں ہوتا
  • وہ blocking calls جو async def handlers کے اندر چھپی ہوتی ہیں — synchronous DB drivers، requests، time.sleep، CPU-bound کام
  • چلتی ہوئی service میں blocked event loop کی حقیقی تشخیص کیسے کی جائے، صرف اندازہ کیسے نہ لگایا جائے
  • عملی fixes: run_in_executor، async-native libraries، اور کب بس def استعمال کر لینی چاہیے

فہرستِ مضامین

  1. بنیادی باتیں
  2. async def بمقابلہ def: FastAPI حقیقت میں کیا کرتا ہے
  3. Threadpool کا حفاظتی جال
  4. وہ blocking calls جو async def کے اندر چھپی ہوتی ہیں
  5. Blocked Event Loop کی تشخیص
  6. اسے ٹھیک کرنا
  7. ایک عملی مثال
  8. عام غلطیاں
  9. Production کی بہترین عملی تدابیر

بنیادی باتیں

ایک Event Loop، ایک Thread، کوئی استثنا نہیں

FastAPI، Starlette پر بنی ہے، اور Starlette asyncio پر۔ default configuration میں ایک single worker process ایک single event loop کو ایک single thread پر چلاتا ہے، اور آپ کی app میں ہر async def coroutine — ہر request handler، ہر dependency، ہر middleware — اسی ایک thread پر باری باری چلتے ہیں۔ “باری باری” صرف اسی صورت کام کرتا ہے جب ہر coroutine وقفے وقفے سے control واپس loop کو دے، جو ہر await پر ہوتا ہے۔ ایک await اور اگلے await کے درمیان، وہ coroutine thread پر مکمل قبضہ رکھتی ہے۔ process میں کوئی اور چیز نہیں چلتی — نہ دوسری requests، نہ health checks، نہ framework کے اپنے internals — جب تک یہ یا تو مکمل نہ ہو جائے یا کسی اور await تک نہ پہنچ جائے۔

آپ کو صرف یہی mental model چاہیے: ایک async def function جو کسی چیز کو await ہی نہیں کرتا، یا کسی synchronous اور سست چیز کو await کیے بغیر call کرتا ہے، وہ صرف آہستہ نہیں چلتا — وہ جتنی دیر لیتا ہے، اتنی دیر تک ہر دوسری in-flight request کو مکمل طور پر روک دیتا ہے۔

یہ “بے ترتیب” سست رفتاری کے طور پر کیوں ظاہر ہوتا ہے

علامت عموماً یہ نہیں لگتی کہ “یہ ایک endpoint سست ہے۔” بلکہ یہ یوں لگتی ہے کہ کوئی بالکل غیر متعلقہ، عام طور پر تیز endpoint کبھی کبھار بغیر کسی ظاہری وجہ کے تین سیکنڈ لے رہی ہے — کیونکہ ان تین سیکنڈز کے دوران وہ اسی thread پر کسی اور کی blocking call کے پیچھے queue میں تھی۔ یہی چیز bug کو ڈھونڈنا مشکل بناتی ہے: slow request اور متاثرہ request عموماً مختلف endpoints ہوتے ہیں، اس لیے متاثرہ endpoint کی الگ profiling کرنے پر اس میں کچھ بھی غلط دکھائی نہیں دیتا۔

async def بمقابلہ def: FastAPI حقیقت میں کیا کرتا ہے

FastAPI ان دونوں signatures کے ساتھ مکمل طور پر مختلف برتاؤ کرتا ہے، اور یہی فرق پوری کہانی ہے:

@app.get("/fast-if-truly-async")
async def get_data():
    # Runs directly on the event loop thread.
    # Every `await` here yields control back to the loop.
    result = await some_async_db_call()
    return result

@app.get("/runs-in-threadpool")
def get_data_sync():
    # FastAPI automatically dispatches this to a worker
    # thread from Starlette's threadpool — it never runs
    # on the event loop thread at all.
    result = some_blocking_db_call()
    return result

ایک async def route براہِ راست event loop پر چلتا ہے۔ FastAPI یہ اعتماد کرتا ہے کہ آپ نے ایسا function لکھا ہے جو درمیان میں await کے بغیر قابلِ ذکر وقت تک thread کو بلاک نہیں کرے گا۔ اس کے برعکس، ایک def route خودکار طور پر run_in_threadpool کے ذریعے ایک الگ worker thread پر offload ہو جاتا ہے — آپ ایک لائن async code لکھے بغیر درست، non-blocking رویہ حاصل کر لیتے ہیں، کیونکہ blocking کام مکمل طور پر کہیں اور ہو رہا ہوتا ہے۔

یہ بالکل اس اندازے کے الٹ ہے جو اکثر لوگ FastAPI سے پہلی شناسائی پر لگاتے ہیں: def کوئی “سست، پرانا” آپشن نہیں ہے۔ واقعی synchronous کام کے لیے — جیسے کسی ایسی library کو call کرنا جس کا کوئی async متبادل نہ ہو، یا کوئی CPU-light blocking I/O — سادہ def اکثر زیادہ محفوظ انتخاب ہوتی ہے، کیونکہ اگر آپ اندرونی implementation میں غلطی بھی کر دیں تب بھی یہ حادثاتی طور پر loop کو بلاک نہیں کر سکتی۔

Threadpool کا حفاظتی جال

Starlette کی threadpool (جو anyio کی پشت پناہی سے چلتی ہے) default طور پر ایک bounded worker pool رکھتی ہے — تاریخی طور پر 40 threads — جس میں def routes اور def dependencies چلتی ہیں۔ def route پر آنے والی ہر request ایک thread لیتی ہے، اپنا blocking کام وہاں کرتی ہے، اور مکمل ہونے پر result event loop کو واپس دے دیتی ہے۔ event loop خود کبھی اس call کے انتظار میں نہیں بیٹھتا؛ وہ بیک وقت دوسری requests کو serve کرتا رہتا ہے۔

یہ واقعی ایک حفاظتی جال ہے، مگر لامحدود نہیں۔ اگر آپ کی service کو 40 سے کہیں زیادہ concurrent سست synchronous requests سنبھالنی پڑیں، تو بعد والی requests کسی خالی thread کے انتظار میں پہلے والوں کے پیچھے queue ہو جاتی ہیں — یہ loop بلاک ہونے سے مختلف failure mode ہے، مگر پھر بھی ایک bottleneck ہے جس سے واقف ہونا ضروری ہے۔ اسے Starlette/anyio کے thread limiter کے ذریعے tune کیا جا سکتا ہے، لیکن حقیقی scale پر زیادہ پائیدار حل عموماً یہ ہوتا ہے کہ آپ پہلے ہی synchronous، thread-hungry کام کی مقدار کم کریں، نہ کہ pool size کو بڑھاتے ہی رہیں۔

threadpool صرف def کے لیے فعال ہوتی ہے۔ async def کے اندر ہونے والی blocking call کو کچھ بھی نہیں پکڑتا — اور یہی پوری وجہ ہے کہ bugs کی یہ قسم وجود رکھتی ہے۔

Threadpool Limiter کو Tune کرنا

اگر آپ کو واقعی default worker-thread حد سے زیادہ threads چاہیے ہوں — مثلاً synchronous file uploads کا کوئی اچانک اضافہ — تو anyio ایک capacity limiter فراہم کرتا ہے جسے آپ startup پر بڑھا سکتے ہیں:

import anyio
from anyio import to_thread

@app.on_event("startup")
async def raise_threadpool_limit():
    limiter = to_thread.current_default_thread_limiter()
    limiter.total_tokens = 100

اسے ایک عارضی تدبیر سمجھیں، حل نہیں۔ ہر اضافی thread memory اور context-switch کا overhead بڑھاتی ہے، اور واقعی blocked event loop کے مسئلے پر اس کا کوئی اثر نہیں ہوتا — یہ صرف تب مدد دیتی ہے جب bottleneck def routes کے لیے thread availability ہو، جو اس مسئلے سے مختلف چیز ہے جس پر یہ پوسٹ بنیادی طور پر بات کر رہی ہے۔

مزید Uvicorn Workers شامل کرنے سے یہ کیوں ٹھیک نہیں ہوتا

ایک عام لیکن غلط ردعمل یہ ہوتا ہے کہ blocked-loop incident سے نکلنے کے لیے مزید Uvicorn/Gunicorn worker processes شامل کر دیے جائیں۔ ہر worker process کو واقعی اپنا الگ event loop ملتا ہے، اس لیے زیادہ workers مجموعی capacity ضرور بڑھاتے ہیں — مگر کسی بھی ایک worker کے اندر بالکل وہی blocking behavior برقرار رہتا ہے: ایک سست async def call اب بھی اس worker کے loop کو دی گئی ہر request کو روک دیتی ہے۔ زیادہ workers incident کے blast radius کو کم کر دیتے ہیں (ایک وقت میں آپ کے صرف 1/N حصے کی traffic stalled worker سے ٹکراتی ہے)، لیکن یہ root cause کو حل نہیں کرتے، اور ایسے bug پر infrastructure cost بڑھا دیتے ہیں جسے پانچ لائن code change سے مفت میں درست کیا جا سکتا تھا۔ اضافی workers کو صرف resilience buffer سمجھیں، blocking call کا اصل حل ہرگز نہیں۔

وہ blocking calls جو async def کے اندر چھپی ہوتی ہیں

خطرناک pattern ہمیشہ ایک ہی شکل میں ہوتا ہے: ایک async def handler جو کسی synchronous چیز کو wrap کیے بغیر call کر دیتا ہے، اور یوں “async” function loop کو تقریباً اتنی ہی شدت سے بلاک کر دیتا ہے جتنی def function کرتی، مگر اس threadpool کے بغیر جو اسے بچا سکتی تھی۔

import time
import requests  # synchronous HTTP client

@app.get("/danger")
async def danger():
    time.sleep(2)                 # blocks the entire event loop for 2s
    resp = requests.get(EXTERNAL) # blocks for however long the network takes
    return resp.json()

process میں کہیں بھی موجود ہر in-flight request ان دونوں calls کے مشترک دورانیے تک رک جاتی ہے۔ عام مجرم یہ ہوتے ہیں:

  • Synchronous HTTP clientsrequests، یا httpx.Client (httpx.AsyncClient نہیں) — جو async def کے اندر استعمال ہوں۔
  • Synchronous database driverspsycopg2، SQLAlchemy کا sync mode — جنہیں async driver کے بجائے براہِ راست call کیا جائے۔ یہی وہ trap ہے جس پر تفصیل سے Async SQLAlchemy Sessions in FastAPI, Done Right میں بات ہوئی ہے: async codebase میں sync engine شامل کرنا یہی blocking behavior دوبارہ لے آتا ہے۔
  • time.sleep() کو asyncio.sleep() کے بجائے استعمال کرنا — ایک آسان typo جس کا اثر تب تک نظر نہیں آتا جب تک concurrent load نہ آئے۔
  • CPU-bound کام — image resizing، PDF generation، بھاری pandas transforms، cryptographic hashing۔ یہ client library سے قطع نظر بلاک کرتے ہیں، کیونکہ لاگت I/O نہیں بلکہ computation ہوتی ہے — “CPU مصروف ہے” کا کوئی async ورژن نہیں، اور await کی کوئی مقدار اسے حل نہیں کرتی۔
  • Local disk پر file I/Oopen()، .read()، .write() سب synchronous system calls ہیں جب تک آپ انہیں aiofiles یا threadpool کے ذریعے نہ چلائیں۔

ان میں سے کوئی بھی error raise نہیں کرتا۔ یہ خاموشی سے process کی ہر دوسری request کو اپنی باری کا انتظار کرواتے رہتے ہیں۔

Blocked Event Loop کی تشخیص

صرف اندازہ لگانا کہ کون سا endpoint مجرم ہے عموماً کام نہیں کرتا — آپ کو چلتے ہوئے process سے حقیقی signal چاہیے۔

1. asyncio debug mode۔ PYTHONASYNCIODEBUG=1 کے ساتھ چلانے سے (یا asyncio.run(main(), debug=True) استعمال کرنے سے) asyncio warning log کرتا ہے جب کوئی callback 100ms سے زیادہ وقت لے، اور یہی blocked loop کی بنیادی علامت ہے۔

PYTHONASYNCIODEBUG=1 uvicorn app.main:app

2. Server.log_slow_callbacks / manual loop instrumentation۔ آپ ایک periodic heartbeat coroutine attach کر سکتے ہیں جو اپنی scheduling delay ناپے — اگر heartbeat جسے ہر 100ms پر چلنا چاہیے باقاعدگی سے دیر سے چل رہی ہو، تو کوئی چیز loop پر قبضہ جمائے بیٹھی ہے:

import asyncio, time

async def loop_monitor():
    while True:
        start = time.monotonic()
        await asyncio.sleep(0.1)
        drift = time.monotonic() - start - 0.1
        if drift > 0.05:
            print(f"Event loop blocked for ~{drift:.2f}s")

3. Mixed endpoint set کے ساتھ load testing۔ کسی معروف تیز endpoint اور کسی مشتبہ سست endpoint کو ایک ساتھ locust یا hey جیسے tool سے hit کریں۔ اگر fast endpoint کی latency، slow endpoint کے load کے ساتھ قدم بہ قدم خراب ہو، تو آپ per-endpoint slowness نہیں بلکہ loop contention دیکھ رہے ہیں۔

4. Thread/task context کے ساتھ APM traces۔ Datadog APM یا OpenTelemetry جیسے tools یہ دکھا سکتے ہیں کہ کوئی span event loop thread پر چل رہی ہے یا threadpool worker پر — child spans کے بغیر ایک سست async def span اس بات کی مضبوط علامت ہے کہ وہ synchronous blocking کام کر رہی ہے۔

5. چلتے ہوئے process پر py-spy dump۔ py-spy کسی چلتے ہوئے Python process کے ساتھ attach ہو جاتا ہے بغیر اسے restart کیے، اور ہر thread کا موجودہ stack print کر دیتا ہے۔ اگر server پھنس گیا ہو تو py-spy dump --pid <uvicorn-worker-pid> چلائیں اور دیکھیں کہ کوئی ایک thread time.sleep، کسی synchronous socket call، یا database driver کی C extension کے اندر بیٹھی ہے — یہی آپ کی blocking call ہے، عین عمل کے دوران پکڑی گئی، production میں، بغیر اس کی reproduction کے لیے code change کیے۔

pip install py-spy
py-spy dump --pid $(pgrep -f "uvicorn app.main:app" | head -1)

اسے ٹھیک کرنا

ایک بار آپ blocking call شناخت کر لیں، تو ترجیحی ترتیب کے مطابق تین حقیقی آپشنز ہوتے ہیں:

1. library کا async-native ورژن استعمال کریں۔ requestshttpx.AsyncClient; psycopg2asyncpg یا SQLAlchemy کا async engine; time.sleepasyncio.sleep۔ تقریباً ہمیشہ یہی درست fix ہوتا ہے — یہ workaround کے بجائے blocking call کو ختم کرتا ہے۔

import httpx

@app.get("/fixed")
async def fixed():
    async with httpx.AsyncClient() as client:
        resp = await client.get(EXTERNAL)
    return resp.json()

2. run_in_threadpool یا run_in_executor کے ذریعے واضح طور پر thread پر offload کریں۔ جب کوئی async متبادل موجود نہ ہو — legacy SDK، C-extension-backed library — تو blocking call کو loop پر چلانے کے بجائے دستی طور پر worker thread پر منتقل کریں:

from starlette.concurrency import run_in_threadpool

@app.get("/legacy-sdk")
async def legacy_sdk():
    result = await run_in_threadpool(legacy_blocking_call, arg1, arg2)
    return result

3. بس اسے def route بنا دیں۔ اگر کوئی handler بنیادی طور پر synchronous ہے — ایک blocking SDK call کرتا ہے اور اس کے علاوہ کوئی concurrent کام نہیں کرتا — تو اسے زبردستی async def میں ڈالنے کا کوئی فائدہ نہیں۔ FastAPI کی threadpool کو یہ سنبھالنے دیں؛ وہ اسی کام کے لیے ہے۔

واقعی CPU-bound کام (image processing، heavy computation) کے لیے نہ threads اور نہ ہی async def حقیقت میں مدد دیتے ہیں، کیونکہ Python کا GIL یہ یقینی بناتا ہے کہ threads CPU-bound code کے لیے parallelism نہیں دیتیں — آپ کو ProcessPoolExecutor چاہیے، یا اس کام کو request path سے مکمل طور پر نکال کر background worker میں منتقل کرنا چاہیے، جس فیصلے پر FastAPI BackgroundTasks vs Celery: Picking the Right One میں بات کی گئی ہے۔

ایک عملی مثال

اثر کو براہِ راست دیکھنے کے لیے concurrent load کے تحت ایک ہی endpoint کے دو versions کا موازنہ کریں۔ دونوں ایک 1-second blocking dependency simulate کرتے ہیں؛ دونوں پر test script سے httpx کے ذریعے بیک وقت 20 requests ماری جاتی ہیں۔

# blocking.py
@app.get("/blocking")
async def blocking():
    time.sleep(1)  # synchronous sleep inside async def
    return {"ok": True}

# fixed.py
@app.get("/fixed")
async def fixed():
    await asyncio.sleep(1)  # yields control back to the loop
    return {"ok": True}

/blocking پر بیس concurrent requests سلسلہ وار مکمل ہوتی ہیں — مجموعی طور پر تقریباً 20 سیکنڈ، کیونکہ ہر time.sleep(1) اگلی request کی processing شروع ہونے سے پہلے واحد working thread پر مکمل قبضہ رکھتی ہے۔ یہی 20 concurrent requests /fixed پر تقریباً 1 سیکنڈ میں مکمل ہو جاتی ہیں، کیونکہ asyncio.sleep فوراً control چھوڑ دیتا ہے اور تمام بیس coroutines loop کے ذریعے ایک ساتھ suspend اور resume ہوتی ہیں۔ endpoint کی اپنی مشتہر latency میں کچھ نہیں بدلا — صرف یہ بدلا کہ آیا وہ process میں چلنے والی دوسری چیزوں کے ساتھ حقیقت میں تعاون کر رہی تھی یا نہیں۔

Concurrency بڑھنے کے ساتھ یہ فرق کم نہیں بلکہ زیادہ ہوتا جاتا ہے۔ دس concurrent requests پر /blocking شاید کسی فوری manual test میں ابھی بھی قابلِ برداشت لگے — تقریباً دس سیکنڈ اتنے خوفناک نہیں لگتے اگر کوئی گھڑی پر زیادہ غور نہ کر رہا ہو۔ مگر حقیقی production service کی traffic levels پر یہی linear-serialization behavior p95 latency chart کو request volume کے ساتھ سیدھی لائن میں اوپر چڑھاتا ہے، بجائے اس کے کہ وہ صحت مند async service کی طرح کسی حقیقی resource limit تک پہنچنے سے پہلے تقریباً ہموار رہے۔ یہی shape — latency کا concurrent load کے ساتھ خطی انداز میں بڑھنا، بجائے اس کے کہ تقریباً flat رہے — dashboard میں blocked event loop کی واضح ترین نشانیوں میں سے ایک ہے، اور عموماً root cause کی manual تشخیص سے کافی پہلے نظر آ جاتی ہے۔

عام غلطیاں

غلطی: یہ سمجھ لینا کہ async def ہمیشہ تیز ہوتی ہے۔ ایسے route کے لیے جس میں ایک ہی synchronous، عملی طور پر non-blocking call ہو، def اور threadpool زیادہ سادہ بھی ہیں اور اتنی ہی محفوظ بھی۔ حل: واقعی synchronous کام کے لیے default طور پر def استعمال کریں، اور async def کو ان code paths کے لیے محفوظ رکھیں جو حقیقت میں کسی چیز کو await کرتے ہوں۔

غلطی: async route میں sync ORM session ملا دینا۔ async def کے اندر sync SQLAlchemy session call کرنا loop کو بالکل ویسے ہی بلاک کرتا ہے جیسے کوئی اور sync call۔ حل: async engine اور session کو مستقل مزاجی کے ساتھ استعمال کریں، جیسا کہ SQLAlchemy sessions والی پوسٹ میں بتایا گیا ہے۔

غلطی: یہ نہ سمجھ پانا کہ اصل bottleneck CPU-bound کام ہے۔ CPU-heavy handler کو async def میں تبدیل کرنا یا run_in_threadpool میں لپیٹنا مدد نہیں کرتا — GIL پھر بھی threads کے درمیان CPU کام کو سلسلہ وار رکھتا ہے۔ حل: CPU-bound کام کے لیے ProcessPoolExecutor استعمال کریں، یا اسے request/response cycle سے مکمل طور پر باہر منتقل کریں۔

غلطی: الگ تھلگ debugging کرنا۔ مشتبہ سست endpoint کو اکیلے، بغیر concurrent load کے test کرنے سے علامت کبھی reproduce نہیں ہوتی — یہ سارا bug contention پر منحصر ہوتا ہے۔ حل: ہمیشہ ایک وقت میں کئی endpoints پر concurrent requests کے ساتھ reproduction کریں۔

غلطی: پہلی ہی response کے طور پر threadpool size بڑھا دینا۔ یہ def-route thread starvation کو چھپا سکتا ہے مگر async def کی وجہ سے blocked event loop پر اس کا کوئی اثر نہیں ہوتا۔ حل: config change کرنے سے پہلے یہ تشخیص کریں کہ آپ کے پاس اصل میں کون سا failure mode ہے۔

Production کی بہترین عملی تدابیر

  • نئی synchronous integrations کے لیے default طور پر def استعمال کریں، async def نہیں۔ FastAPI کی threadpool کو اپنا کام کرنے دیں، بجائے اس کے کہ آپ وہی protection خود بنانے کی کوشش کریں۔
  • ہر async def کا audit کریں کہ اس میں کوئی ایسی call تو نہیں جو await نہیں کی گئی۔ requests.، time.sleep(، یا async handlers کے اندر synchronous driver imports کے لیے ایک فوری grep اکثر حقیقی incidents کو deploy ہونے سے پہلے پکڑ لیتا ہے۔
  • Production میں loop-lag monitoring شامل کریں، صرف local میں نہیں۔ scheduling drift log کرنے والی ایک ہلکی heartbeat coroutine تقریباً کچھ خرچ نہیں کرتی اور regressions فوراً پکڑ لیتی ہے۔
  • Single requests نہیں، concurrency کے ساتھ load test کریں۔ تعریف کے مطابق loop contention sequential testing میں نظر نہیں آتی۔
  • پہلے app کی بنیادی ساخت درست کریں۔ ایسی structure جو route handlers کو ہلکا رکھتی ہو — جیسا کہ FastAPI Project Structure That Survives Growth میں بتایا گیا ہے — stray blocking call کو پہچاننا بہت آسان بنا دیتی ہے، کیونکہ business logic ایک واضح جگہ پر ہوتی ہے، handlers میں بکھری نہیں ہوتی۔

خلاصہ

Blocked event loop کوئی پراسرار performance ceiling نہیں — یہ ایک مخصوص، قابلِ سراغ نتیجہ ہے کہ ایک coroutine اس واحد thread کو پکڑے رکھتی ہے جس پر پورا process انحصار کرتا ہے۔ async def بمقابلہ def کا معاملہ کبھی اس بات پر نہیں تھا کہ کون سا “جدید” ہے؛ اصل سوال یہ ہے کہ آیا آپ FastAPI کو ایسا code دے رہے ہیں جو loop کے ساتھ حقیقت میں تعاون کرتا ہے، یا ایسا code جو صرف بظاہر ایسا لگتا ہے۔ Load testing سے پہلے blocking calls کا audit کر لیں، اور اگر شک ہو تو threadpool کو یہ سنبھالنے دیں — def کوئی fallback آپشن نہیں، اکثر یہی درست آپشن ہوتی ہے۔

کیا آپ نے واقعی اپنی async def routes کو concurrency کے تحت load-test کیا ہے، یا بس یہ فرض کر لیا ہے کہ keyword ہی آپ کے لیے سارا کام کر رہا ہے؟

مزید مضامین