Picture this. A creator posts a Reel that says "comment GUIDE and I'll send it to you." It does numbers. Within an hour, 10,000 people comment.
If your automation was built for the demo, this is the moment it falls over.
I've been building on the Instagram Graph API, and the happy path is easy: webhook in, DM out. The hard part is what happens when the happy path gets 10,000 times busier than you tested. Here's what I learned.
Problem 1: Webhooks arrive in bursts, not a trickle
When someone comments, Meta sends your server a webhook. When 10,000 people comment, Meta sends 10,000 webhooks in a very short window.
The tempting first version does the work inside the request handler: look up the account, check the automation, write to the database, call the API to send the DM, then return 200.
That falls over for two reasons:
- Your database becomes the bottleneck. Every webhook opens connections and runs queries at exactly the rate Meta delivers events, not the rate you can handle.
- Slow responses trigger retries. Meta retries deliveries that time out or fail. A struggling server gets more traffic, not less. That's a retry storm, and it's how a slow minute becomes an outage.
The fix: do almost nothing in the handler.
def webhook(request):
if not valid_signature(request): # verify the HMAC signature first
return HttpResponse(status=403)
for entry in request.json()["entry"]:
queue.enqueue(process_entry, entry) # hand off to a background worker
return HttpResponse(status=200) # ack immediately
(Simplified example, not production code.)
Verify the signature, enqueue, return 200. The handler's job is to say "got it" quickly. Workers then drain the queue at whatever pace your system can sustain, no matter how fast events arrive.
One more rule: be careful about returning errors under load. If your own database is struggling, a 5xx just invites a retry on top of the problem. Acknowledge, buffer, and recover.
Problem 2: Meta has limits, and they're not one number
Sending a DM isn't unlimited. Depending on how you send, you can hit several different ceilings:
Per-account hourly caps on how many DMs you can send as replies to comments.
App-level usage limits shared across every account your app serves.
The messaging window: automation is meant to respond to something the user did first, and replies live inside a time window.
The app-level limit is the sneaky one. One viral creator can burn through capacity that every other customer depends on.
Meta tells you how close you are. Graph API responses include an X-App-Usage header with usage percentages. Read it. Cache it. Then use it as a brake:
def safe_to_send(account):
usage = cache.get("app_usage") # refreshed from X-App-Usage
if usage and usage["call_count"] >= SAFETY_MARGIN:
return False # app-wide: slow down for everyone
return hourly_sent(account) < HOURLY_CAP
(Simplified example. Pick your own margins and stay well under the documented limits instead of riding the edge.)
A tip that sounds obvious and isn't: keep that usage reading fresh even when you're idle. If something else shares your app (a test script, a second environment), real usage can climb while your cache sits stale. A cheap periodic ping fixes it.
Problem 3: What do you do when you're over the limit?
You don't drop the message. You also don't hammer the API until it pushes back.
You park it and retry later:
def process_trigger(trigger):
if not safe_to_send(trigger.account):
trigger.status = "queued" # park it, oldest first
return
send_dm(trigger)
A scheduled job picks queued items back up when the window resets, in the order they arrived. Oldest first matters: the person who commented first should get their DM first.
The user-facing result: instead of "the automation broke", a creator sees "your DMs are sending in order and will finish shortly." That's an honest, calm experience, and it keeps accounts out of trouble.
Problem 4: Two workers, one trigger
When you scale workers, you'll eventually have two of them pick up the same job. Without a guard, someone gets two DMs. Two identical DMs is the fastest way to look like spam.
The fix is to claim the job atomically before doing any slow work:
with transaction.atomic():
trigger = (Trigger.objects
.select_for_update()
.filter(id=trigger_id, status__in=["pending", "queued"])
.first())
if trigger is None:
return # someone else already has it
trigger.status = "processing"
trigger.save()
# lock released; now do the slow API call
Problem 5: Tokens expire and permissions get revoked
Instagram access tokens don't last forever. Long-lived tokens last about 60 days and can be refreshed before they expire. Run a daily job that refreshes tokens approaching expiry, or automations will silently die weeks later.
Users can also revoke access at any time. When the API returns an invalid-token error (code 190), stop sending for that account, mark it disconnected, and tell the user. Retrying a revoked token just generates noise and errors.
The checklist
If you're building DM automation, here's what I'd do again
- Verify signatures, ack fast, queue everything. Do almost nothing in the webhook handler.
- Read X-App-Usage and slow down early. Stay well under limits, not at them.
- Queue, don't drop. Retry oldest-first when capacity returns.
- Claim jobs atomically. One trigger, one DM.
- Refresh tokens on a schedule and handle revocation cleanly.
- Only message people who engaged first. That's both Meta's rule and good product sense.
None of this is glamorous. But the first time a post goes viral and your system quietly works through 10,000 comments without a single complaint, you'll be glad it's there.
We build this at
,an DM automation platform built on Meta's official API. If you have questions about any of the above, drop them in the comments.
Top comments (0)