Webhook, WebSocket or Polling? Pick by Failure Mode
Webhook, WebSocket or Polling? Pick by Failure Mode
People search for "Twitter webhook" expecting the pattern they know from Stripe or GitHub: register a URL, receive a POST when something happens, done.
That pattern doesn't exist in X's public API for arbitrary accounts. X's Account Activity API does webhooks, but only for accounts that have authorised your app through OAuth — your own account, or users who went through your login flow. For "tell me when this public account posts," X gives you nothing to register.
Third-party providers can and do fill that gap, ours included. So the real question isn't whether a webhook is possible — it's which of the three mechanisms fails in a way you can live with.
If you only want the comparison, the table below is the article. There's a free demo endpoint if you want to test response shapes before designing around them.
The three options
| Polling | WebSocket | Webhook | |
|---|---|---|---|
| From X directly, public accounts | Yes | No | No |
| Via a data provider | Yes | Yes | Yes |
| Typical latency | Half your interval | Seconds | Seconds |
| Who holds the connection | Nobody | You | Them |
| Fails by | Missing events between polls | Dropped connection | Your endpoint being down |
| Survives your restart | Trivially | Needs reconnect logic | Trivially |
| Works behind NAT / localhost | Yes | Yes | No — needs a public URL |
| Fits serverless | Yes | Badly | Yes |
That last row is usually the deciding one. A WebSocket wants a long-lived process; a Lambda or a Cloud Function doesn't have one. If your consumer is serverless — or Zapier, n8n, a Telegram bot — the webhook isn't a preference, it's the only shape that fits.
Polling: fine more often than people admit
Polling has a worse reputation than it deserves. If your tolerance is "within a minute or two," it's simpler, easier to reason about, and recovers from failure by doing nothing special.
import requests, time
API = "https://api.socialapi.tech/v1"
HEADERS = {"X-API-Key": "your_key"}
seen = set()
def poll(username, interval=60):
while True:
r = requests.get(f"{API}/user/last_tweets",
params={"username": username, "limit": 20},
headers=HEADERS, timeout=30)
if r.status_code == 429: # backed off, not broken
time.sleep(interval * 2); continue
r.raise_for_status()
for t in reversed(r.json().get("data") or []):
if t["id"] not in seen:
seen.add(t["id"])
handle(t) # oldest first, so order reads sanely
time.sleep(interval)
Three details in there that aren't obvious.
reversed() — the API returns newest first, so a naive loop delivers your events backwards. For a notifier nobody notices; for anything reconstructing a conversation it's a bug that takes a while to find.
The seen set — polling windows overlap by design. You will see the same tweet on consecutive polls, and deduplication is yours.
Backing off on 429 rather than failing — a rate limit is temporary, and the right response is simply to go later. Telling the three kinds of limit apart is covered in what rate limit exceeded actually means.
Where polling genuinely breaks: an account posting five times in ninety seconds, polled every five minutes with limit=20. You'll catch all five. Raise the posting rate or lower the limit and you start dropping the middle of bursts — silently, with nothing in your logs to tell you.
WebSocket: lowest latency, you own the connection
A WebSocket gets you seconds instead of minutes. The trade is that you now own a connection that will drop — not might, will. Networks reset, balancers recycle, containers move.
import json, time, websocket # websocket-client
def run(url, headers, on_tweet):
backoff = 1
while True:
try:
ws = websocket.create_connection(url, header=headers, timeout=30)
backoff = 1 # reset only after a real connect
while True:
msg = ws.recv()
if not msg: # server closed
break
on_tweet(json.loads(msg))
except Exception as e:
print("stream dropped:", e)
finally:
try: ws.close()
except Exception: pass
time.sleep(backoff)
backoff = min(backoff * 2, 60) # cap it
Reset the backoff after a successful connect, not a successful message. Resetting on message means a connection flapping every few seconds hammers the server; resetting on connect backs off properly.
The question that shapes your architecture: what happens to events during the sixty seconds you were disconnected? A pure stream doesn't replay them. If gaps are unacceptable, run a low-frequency poll alongside the stream and deduplicate by tweet ID — you get latency and completeness for a few extra lines.
Webhook: when you can't hold a connection
If your consumer can't keep a socket open, a provider-side webhook is the right shape. You register a URL; new tweets arrive as POSTs.
Here's what a competent implementation handles, and it's worth checking any provider against this list — including ours:
Signing. Your endpoint is public and will be probed. Ours sends an HMAC-SHA256 over {timestamp}.{body} as X-SocialAPI-Signature: sha256=…, with X-SocialAPI-Timestamp alongside it. Verify it before trusting the payload, and reject anything with a stale timestamp — that's what stops a captured request being replayed at you later.
import hmac, hashlib
def verify(body: bytes, timestamp: str, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(),
f"{timestamp}.".encode() + body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)
Use compare_digest, not ==. String comparison short-circuits on the first differing byte, which leaks the signature a character at a time to anyone patient enough to measure.
Retries. A deploy takes thirty seconds; without retries that's every event in the window gone. Ours retries on a 1s / 3s / 7s backoff before giving up, with a 6-second delivery timeout per attempt. Ask any provider what theirs is — "we POST once" is a real answer and a real limitation.
Idempotency on your side. Retries produce duplicates by definition. Each delivery carries X-SocialAPI-Delivery as a stable ID; key on it, or on the tweet ID, and make the handler safe to run twice. This is the same discipline as the polling seen set, just at the other end of the wire.
One practical note: we accept a Telegram bot token directly and resolve it to the right URL, because "post new tweets into a Telegram channel" is common enough that making people build a bridge for it is silly. You can register up to five endpoints per account.
Choosing, in one pass
- Is a minute of latency fine? → Poll. Stop here; you'll maintain less code.
- Need seconds, and you have a long-lived process? → WebSocket, with reconnect and capped backoff.
- Need seconds, but your consumer is serverless or a third-party tool? → Webhook.
- Can you lose events during an outage? → If no, add a periodic poll alongside whichever you picked, and deduplicate.
Most projects that ask for a webhook on day one are answered by step 1. Most that genuinely need one are answered by step 3 — and the giveaway is that their consumer has no process to hold a socket.
We run both delivery paths: a WebSocket subscription billed per account per hour, and webhooks with the signing and retry behaviour described above. The live feed walkthrough covers the display side, and monitoring X keywords covers the query side. Signing up with Google includes $0.20 in free credits, no card — enough to wire up both and see which fits.
FAQ
Does Twitter have a webhook API? X's Account Activity API supports webhooks, but only for accounts that have authorised your app via OAuth. There's no X-native webhook for arbitrary public accounts — that gap is what third-party providers fill.
How do I get notified when someone tweets? Poll their timeline on an interval, subscribe over WebSocket, or register a webhook with a provider. All three work for public accounts; the last two get you single-digit seconds.
What's the difference between a webhook and a WebSocket? Direction and connection ownership. A webhook is the provider POSTing to your public URL. A WebSocket is your client holding an open connection. The WebSocket works behind NAT; the webhook needs a reachable endpoint but survives your restarts for free.
Can I use a webhook for public tweets? Not from X directly, but yes through a provider that offers them. Ours does, with HMAC signing and automatic retries.
How do I verify a webhook signature?
Recompute the HMAC-SHA256 over {timestamp}.{body} with your secret and compare against the signature header using a constant-time comparison. Reject stale timestamps to prevent replay.
Why use compare_digest instead of ==? Ordinary string comparison exits at the first differing byte, so response timing leaks the signature one character at a time. Constant-time comparison removes that channel.
What happens if my webhook endpoint is down? Depends on the provider. Ours retries on a 1s/3s/7s backoff with a 6-second timeout per attempt. Always ask — "we POST once" is a common and consequential answer.
Should my webhook handler be idempotent? Yes. Retries and reconnects both produce duplicates. Key on the delivery ID or tweet ID and make a second run a no-op.
How fast is a WebSocket stream? Seconds rather than minutes. Ask whether a provider's "real-time" is a genuine push or a frequent poll behind the API — the difference shows in both latency and cost.
Is polling bad practice? No. It's correct whenever a minute of latency is acceptable. Fewer failure modes, free recovery from restarts, and far easier to debug.
How often should I poll an account? Match it to posting rate and your delay tolerance. Every 60s suits most monitoring. For an active account, make sure the page size covers bursts between polls.
Why do I keep receiving the same tweet twice? Polling windows overlap, stream reconnects replay, and webhooks retry. All three produce duplicates — deduplicate by tweet ID.
Do I need a public URL for a WebSocket? No. The client opens an outbound connection, so it works from a laptop, behind NAT, or in a container with no inbound ports.
Can I run a WebSocket in a serverless function? Not well — functions are short-lived and streams want a long-lived process. Use a webhook instead; that's exactly the case it exists for.
What happens to tweets while my stream is disconnected? A pure stream doesn't replay them. If gaps matter, run a slower poll alongside and deduplicate.
How should I handle stream reconnects? Exponential backoff with a cap, resetting only after a successful connect. Resetting after a message makes a flapping connection hammer the server.
Can I send tweets straight to Telegram or Discord? Yes — that's a standard webhook target. We accept a Telegram bot token directly and resolve it, since it's a common enough case that building a bridge for it is wasted work.
How many webhook endpoints can I register? Five per account on ours. Enough for separate dev/prod endpoints plus a couple of notification targets.
Can I subscribe to a hashtag rather than an account? Account subscriptions are the usual unit for streams and webhooks. For a hashtag or search term, poll a search endpoint on an interval — see monitoring X keywords.
How is streaming priced compared to polling? Polling costs per request whether or not anything happened. Streaming is typically per subscribed account over time. For a quiet account watched continuously, streaming usually wins; for occasional checks, polling does.
Do I still need rate limit handling with a stream or webhook? Less for the delivery itself, since you're not making repeated requests. But any REST calls alongside it still need backoff, and subscription management is ordinary HTTP.
What if I need both speed and guaranteed completeness? Run both: push for latency, a slower poll for gaps, deduplicate by ID. It's the standard pattern and costs a few lines rather than an architecture.
How do I test a webhook before pointing it at production? Register a request-inspection URL first and fire the test delivery, so you can see the exact headers and body. Ours has a test endpoint for precisely this — verify your signature code against a real delivery, not a hand-built one.
Should my webhook return 200 before or after processing? Return 200 as soon as you've durably stored the payload, then process asynchronously. Doing the work before responding risks a timeout, which triggers a retry and gives you the same event twice.
What happens if my endpoint is slow rather than down? It's treated as a failed delivery once the timeout passes — ours is 6 seconds per attempt. A slow handler and a dead one look the same from the sender's side, which is the reason to acknowledge first and work afterwards.
Pick by failure mode, not by latency numbers — that's the one idea worth taking from this. If you want to try both delivery paths against real traffic, a Google sign-up includes $0.20 in free credits and no card.