instagrapi

Deploy instagrapi in FastAPI: async, aiograpi, background tasks

Maintained by the instagrapi contributors · Library on GitHub

Updated

You are wiring an Instagram surface into a FastAPI app — a /users/{username} endpoint that returns a normalized profile, a webhook that schedules a DM in response to a Stripe event, a /posts/{shortcode} lookup the dashboard polls every few seconds — and instagrapi looks like the easy answer. The first iteration almost always lands on a synchronous Client() constructed at module import time, a few async def handlers that await nothing but call straight into cl.user_info_by_username(), and a brief celebration when the curl call succeeds. The celebration ends when the second concurrent request arrives. Latency for everyone watching the API doubles, then triples, then settles into a pattern where every endpoint feels slow whenever someone touches Instagram. The cause is not Instagram, the network, or instagrapi: it is the runtime model. FastAPI assumes handlers cooperate with the event loop. Sync instagrapi does not cooperate.

This page walks the FastAPI-specific shape of that problem and the four ways to dissolve it: switch to the async-native sibling aiograpi, push sync calls into a thread pool with run_in_executor, lean on BackgroundTasks for fire-and-forget work, and hand long-running IG jobs off to Celery once BackgroundTasks is no longer enough. Each option is correct in some shape; the trick is matching the option to the workload, and the FastAPI primitives — Depends, BackgroundTasks, the lifespan context — give you the slots to do that cleanly.

Setup

Install the FastAPI stack alongside aiograpi and a Redis client. aiograpi is the async-native counterpart maintained by the same author as instagrapi, with the same call surface; if your codebase is already async def end to end, prefer it from the first commit.

pip install fastapi 'uvicorn[standard]' aiograpi redis httpx

Use FastAPI’s lifespan context to hold a single Instagram client per process and bind it via Depends, instead of constructing fresh clients per request. A per-request Client() regenerates the device fingerprint on every call, which makes Instagram’s risk model treat each request as a brand-new device — exactly the trust signal you want to avoid.

# main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends
from aiograpi import Client

@asynccontextmanager
async def lifespan(app: FastAPI):
    cl = Client()
    # session load + login goes here, awaited
    app.state.ig = cl
    yield
    # graceful shutdown: dump session back to Redis

app = FastAPI(lifespan=lifespan)

async def get_ig() -> Client:
    return app.state.ig

Treat credentials as environment variables and never log them; the lifespan callback is also the right place to load the session blob from Redis before await cl.login() so the device fingerprint persists across deploys. If you skip the persistence step, every deploy looks like a new device to Instagram and you will burn through challenges fast.

Working example

The minimum end-to-end shape is one async endpoint that fetches profile data through the lifespan-bound client. The handler awaits the IG call, the runtime stays free to service other coroutines while the network round-trip is in flight, and there is no thread-pool ceremony anywhere in the request path.

# routes.py
from fastapi import APIRouter, Depends, BackgroundTasks
from aiograpi import Client

router = APIRouter()

@router.get('/users/{username}')
async def user_info(username: str, cl: Client = Depends(get_ig)):
    user = await cl.user_info_by_username(username)
    return {
        'pk': user.pk,
        'full_name': user.full_name,
        'follower_count': user.follower_count,
    }

Three small details make this snippet production-shaped. The dependency injection pulls the long-lived client from app.state rather than minting one per request. The handler is fully async def with an awaited IG call, so the event loop stays cooperative. The response is the small projection of the model the dashboard actually needs, not the full User object — keeping the JSON narrow keeps p99 latency stable when the upstream object grows fields you do not consume.

For comparison, here is the same endpoint written naively against sync instagrapi inside an async handler. It runs, returns the right answer, and quietly destroys p99 for every other endpoint sharing the worker.

# DON'T do this in a FastAPI async handler
from instagrapi import Client as SyncClient
sync_cl = SyncClient()  # module-level, sync

@router.get('/users-bad/{username}')
async def user_info_bad(username: str):
    # This call BLOCKS the event loop for the full Instagram round-trip.
    user = sync_cl.user_info_by_username(username)
    return {'pk': user.pk}

The fix when the codebase is committed to sync instagrapi is await asyncio.to_thread(...) (Python 3.9+) or await loop.run_in_executor(None, ...). Either offloads the blocking call to FastAPI’s thread pool, which lets the event loop keep servicing other coroutines, at the cost of consuming a thread per concurrent IG call. That is fine for a handful of concurrent endpoints; it falls over fast under burst load because the thread pool is small by default.

Production caveats

Three patterns repeatedly break FastAPI integrations once they leave a developer laptop. They are roughly in order of how often they cost a team a Friday afternoon.

1. Sync instagrapi inside async handlers blocks the loop

This is the rookie bug and the one that bites hardest because it is silent in tests. A handler that uses sync instagrapi from async def will pass every functional test you write, return the right JSON, and look fine in dev. The damage shows up only under concurrency: the event loop pauses for the duration of every IG call, and every other coroutine the worker is servicing pauses with it. The fix is to either switch to aiograpi (preferred) or wrap each sync call in asyncio.to_thread so the blocking work runs off the loop. Wrapping is cheaper to write but caps your concurrency at the size of the default thread pool — usually 40 — which is much lower than uvicorn’s coroutine ceiling.

2. Per-request Client instances waste device trust

A Client() constructor regenerates the device fingerprint and timezone offset every time. Constructing one per request looks innocent in a Depends callback but it is what causes a slow drip of challenge_required errors across the fleet: Instagram sees the same account logging in from a parade of different phones a few seconds apart and gates the account. Hold the client at the lifespan layer, share it via app.state, and treat fingerprint generation as a one-shot at first boot.

3. BackgroundTasks vs Celery — pick by duration

BackgroundTasks runs the callback in the same uvicorn process after the response is flushed. That is perfect for a 200-millisecond comment post triggered by a webhook: the webhook returns 202 immediately, the IG call runs on a worker thread, the operation completes well inside the request lifetime. It is the wrong tool the moment the IG work outlives the request — a 30-second user_followers walk inside a BackgroundTask ties up a uvicorn worker thread for 30 seconds, which is functionally identical to blocking the request in the first place. Once any IG operation routinely exceeds a second or two, push it to Celery and treat the FastAPI handler as a thin enqueue layer.

Fix in instagrapi

Four steps, in order — each one assumes the previous one is in place.

  1. Use aiograpi for async endpoints. If the codebase is already async, switch the import: from aiograpi import Client. The call surface matches instagrapi closely enough that handlers usually need only the await keyword added. Keep the same session blob — aiograpi reads the dictionary instagrapi writes, so a session minted by one library loads cleanly into the other.

  2. asyncio.to_thread if you must keep sync instagrapi. When the rest of the codebase is sync and a rewrite is not on the table, wrap each call: user = await asyncio.to_thread(sync_cl.user_info_by_username, username). The handler becomes async-cooperative, but the per-call overhead is one thread-pool slot for the duration of the IG round-trip. Watch the FastAPI thread-pool size and raise it past the default once concurrent IG calls grow.

  3. Lifespan-scoped client + Depends for lifecycle. Bind the Instagram client at lifespan setup, store it on app.state.ig, and pull it into handlers via Depends. This gives one client per uvicorn process — exactly the lifecycle that keeps the device fingerprint stable. On graceful shutdown, dump the post-session settings back to Redis so the next process inherits the cookie jar.

  4. BackgroundTasks for short actions, Celery for long. Reach for BackgroundTasks when the work fits inside a worker-thread budget of one or two seconds and is genuinely fire-and-forget. Reach for Celery the moment IG work needs durability across deploys, retries on please_wait_a_few_minutes, or a queue you can scale independently of the web tier. The Celery integration page covers the worker-side pattern in detail.

Deep dive

The choice between aiograpi and instagrapi + asyncio.to_thread is not just stylistic — the runtime cost differs and the difference matters under load. aiograpi awaits the network round-trip on the event loop directly, which means a thousand concurrent waiting calls cost a thousand small coroutine frames and zero threads. The same workload on instagrapi + to_thread consumes a thousand thread slots, which is two-and-a-half orders of magnitude over the default thread pool. In practice, to_thread is a fine bridge for codebases that have a handful of IG-touching endpoints and a low concurrent-request budget; it is the wrong long-term answer for an API gateway that fans out to Instagram on every dashboard render.

The longer-term answer for any team running FastAPI at scale is the same architectural shape Django teams converge on: keep instagrapi out of the request path entirely. The FastAPI handler enqueues a Celery task and returns a 202 with a task id; the worker tier owns instagrapi, the session lifecycle, the rate-limit budget, and the retry policy. The web tier becomes stateless against Instagram and can be scaled, deployed, and rebooted without disturbing in-flight IG work. That separation is nearly free to add on day one and astonishingly painful to retrofit later, so if there is any doubt the workload will grow, set up the Celery seam before the first real endpoint ships.

Related integrations

Related errors

Related guides

Frequently asked

Why does instagrapi block my FastAPI event loop?

instagrapi is sync — every cl.user_info_by_username() call uses requests under the hood and blocks the calling thread. In a FastAPI async handler, that thread IS the event loop, so all other handlers stall until the IG call returns.

Should I use aiograpi instead of instagrapi in FastAPI?

Yes, for production. aiograpi is the async sister library — same author, same API surface, async/await native. It is a near drop-in for asyncio-based FastAPI codebases. instagrapi can still work via run_in_executor, but you trade thread-pool complexity for the same end result.

What does FastAPI's BackgroundTasks give me with instagrapi?

It dispatches the IG call AFTER the response is sent — the FastAPI client gets a 202 immediately while the IG call runs on a worker thread. That is the right primitive for fire-and-forget actions like posting a comment. For long scrapes, push to Celery instead.

Can I share an instagrapi session across FastAPI workers?

Same problem as Django — multiple uvicorn workers means multiple processes. Persist the session to Redis or Postgres and lock the refresh path. The FastAPI guide for instagrapi sessions matches the general session-persistence guide.

Skip the infra?

Managed Instagram API — same endpoints, sessions and proxies handled.

Try HikerAPI → Full comparison
More from the team