WEIR

Twitter Streaming API in Python: Filtered Stream (2026)

Use the Twitter streaming API in Python: add X filtered stream rules, read posts with reconnects and backoff, see the cost, and know when to poll instead.


Published · Updated · 7 min read

Cover image: A stream that reconnects itself

The Twitter streaming API in Python is X's filtered stream: you add rules with one POST, then read matching posts from a long-lived GET.

This guide is for Python developers who need X (Twitter) posts within seconds. You get X's limits and prices as of October 2026, tested code, and a cheaper route if minutes will do. We run Weir, which polls.

Add your first rule:

curl -X POST https://api.x.com/2/tweets/search/stream/rules \
  -H "Authorization: Bearer $X_BEARER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"add": [{"value": "\"acme rockets\" -is:retweet", "tag": "acme"}]}'
  1. Add a rulePOST the rules endpoint, with a tag.
  2. List and delete rulesGET the list, delete by id.
  3. Read the streamOne GET that stays open.
  4. Reconnect with backoffX's waits and a 21-second timeout.

Step 1: Set up Python for the Twitter streaming API

You need an X developer account, an app in the Developer Console, its bearer token and prepaid credits. Install requests, export the token as X_BEARER_TOKEN, and open one session:

import json, os, time, requests

API = "https://api.x.com/2/tweets/search/stream"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['X_BEARER_TOKEN']}"
NET_ERRORS = (requests.ConnectionError, requests.Timeout,
              requests.exceptions.ChunkedEncodingError)

We hold no X API key, so we tested the X code against a local stub that serves the JSON shapes from X's docs.

Step 2: Add X API stream rules

A rule is a search query that X checks against each new post. Give every rule a tag, so you can tell which one matched:

def add_rule(value, tag):
    r = session.post(f"{API}/rules", json={"add": [{"value": value, "tag": tag}]}, timeout=30)
    r.raise_for_status()
    for problem in r.json().get("errors", []):  # a duplicate or invalid rule
        print("rule not added:", problem.get("title"), problem.get("value"))
    return r.json().get("data", [])

print(add_rule("cat has:images", "cats with images"))

Read errors even on a 200: X returns duplicate and invalid rules there. Add ?dry_run=true to check a rule without saving it.

The syntax, from X's "Build a rule" page:

  • A space means AND, OR means or, and - excludes. AND binds first, so use brackets.
  • Some operators can't stand alone. has:media by itself is refused; pair it with a word.
  • Case doesn't matter, but accents do. diacrítica won't match "diacritica".

The limits that matter, from X's docs as of October 2026:

Limit Pay-per-use Enterprise
Rules per project 1,000 25,000+
Rule length 1,024 characters 2,048 characters
Stream connections 1 per app 2 redundant
Backfill on reconnect None Up to 5 minutes
Recovery None Last 24 hours

X's rate-limit page adds a cap of 50 connection attempts per app every 15 minutes.

Step 3: List and delete stream rules

X keeps your rules between connections, and you can change them mid-stream. List them before you add more:

def list_rules():
    r = session.get(f"{API}/rules", timeout=30)
    r.raise_for_status()
    return r.json().get("data", [])

def delete_rules(ids):
    r = session.post(f"{API}/rules", json={"delete": {"ids": ids}}, timeout=30)
    r.raise_for_status()
    return r.json()["meta"]["summary"]

Step 4: Read the Twitter stream in Python with reconnects

X sends each post as a line of JSON, plus a blank line every 20 seconds to keep the GET alive. Drops will happen, X warns, so the loop reconnects itself:

def stream(on_message, net_wait=0.0, http_wait=5):
    while True:
        try:
            with session.get(API, stream=True, timeout=(10, 21)) as r:
                if r.status_code == 200:
                    net_wait, http_wait = 0.0, 5
                    for line in filter(None, r.iter_lines()):  # skip keep-alives
                        on_message(json.loads(line))
                    continue  # closed by X: reconnect now
                wait = max(http_wait, 60) if r.status_code == 429 else http_wait
                http_wait = min(wait * 2, 320)
        except NET_ERRORS:
            wait, net_wait = net_wait, min(net_wait + 0.25, 16)
        print(f"reconnecting in {wait} s")
        time.sleep(wait)

The waits come from X's "Handling disconnections" page:

  • Network errors retry at once, then 250 ms slower each time, up to 16 seconds.
  • HTTP errors wait 5 seconds, doubling to 320; a 429 starts at a minute.

The 21-second read timeout catches a silent line, since X says to reconnect after 20 quiet seconds. Now print what arrives:

def show(message):
    if "data" in message:
        print(message["data"]["id"], message["data"]["text"])
    else:
        print("stream message:", message.get("errors", message))

stream(show)

We ran it on 22 October 2026. The stub sent two posts, dropped the line, answered 503, went silent for 25 seconds, then sent an in-stream error:

1234567890 Look at this cute cat! 🐱
1234567891 Look at this cute cat! 🐱
reconnecting in 0.0 s
reconnecting in 5 s
1234567892 Look at this cute cat! 🐱
reconnecting in 0.0 s
stream message: [{'title': 'operational-disconnect', ...}]

Every drop ended in a fresh connection. Keep show() fast: X says a client that reads too slowly gets cut off.

Step 5: Use Tweepy's StreamingClient instead

Tweepy 4.17.0, released on 2 July 2026, still ships StreamingClient, and its source uses the same waits:

import os, tweepy

class Printer(tweepy.StreamingClient):
    def on_tweet(self, tweet):
        print(tweet.id, tweet.text)

    def on_errors(self, errors):
        print("stream message:", errors)

client = Printer(os.environ["X_BEARER_TOKEN"])
client.add_rules(tweepy.StreamRule("cat has:images", tag="cats with images"))
client.filter()

It rode out the same drops. From its source:

  • Tag every rule. Its on_data reads rule["tag"] for each match.
  • An error in your callback ends the stream. Wrap on_tweet in try.

It still calls api.twitter.com, which answered our test request on 22 October. Our Tweepy tutorial covers the rest.

How much does the X filtered stream cost?

X's billing page counts each unique post the stream delivers as a post read. At $0.005 a post, your bill tracks how busy your rules are:

X filtered stream, cost per 30 days (October 2026)
100 posts a day$15
1,000 posts a day$150
10,000 posts a day$1,500

A post delivered twice in one UTC day is billed once. Pay-per-use stops at 3 million post reads a month, and X's own example says happy alone matches over 200,000 posts a day.

Don't need seconds? Poll with sinceId or a Weir monitor

Weir doesn't stream. If minutes are fine, poll a search and pay only for new posts. Weir's POST /v1/x/search takes sinceId, which returns only posts newer than that id:

URL = "https://api.weirapp.io/v1/x/search"
HEADERS = {"Authorization": f"Bearer {os.environ['WEIR_API_KEY']}"}

def poll(query, since_id=None, max_pages=3):
    body = {"query": query, "limit": 20, **({"sinceId": since_id} if since_id else {})}
    posts = []
    for _ in range(max_pages):
        r = requests.post(URL, json=body, headers=HEADERS, timeout=60)
        r.raise_for_status()  # 402 means out of credits: stop
        posts += r.json()["data"]
        if not r.json()["meta"]["nextCursor"]:
            break
        body["cursor"] = r.json()["meta"]["nextCursor"]
    return posts, max((p["id"] for p in posts), key=int, default=since_id)

Call it on a timer and pass back the id it returns. We ran one poll on 22 October 2026: @XDevelopers' original posts newer than a 7 October post, limit 2. It cost 3 credits:

2111172163776708859  2026-10-16  120 likes
2110414269036794108  2026-10-14  116 likes

The first id is your next sinceId. The response also held a cursor, so a full poll pages on, as our X API pagination guide explains.

A poll costs 1 credit even when nothing is new, so your timer sets a floor:

Weir credits per 30 days just to poll, before any posts
Every minute43,200
Every 5 minutes8,640
Every 15 minutes2,880
Hourly720

Each post found adds 1 credit, plus usually 1 for the empty page that ends the poll. The floor of a 15-minute poll fits in the 5,000 free credits a month.

A Weir monitor runs the timer for you, hourly or daily, and keeps only new posts. It reads about 20 per run, and hourly runs need any $5 pack. Our keyword alerts guide sets one up, and the Twitter firehose guide compares every option.

If the stream doesn't work

  • 401 on connect: the bearer token is wrong. Copy it again from the Developer Console.
  • Nothing arrives: run list_rules(). No rules, or a rare phrase, means a quiet stream.
  • 429 on connect: you made over 50 connection attempts in 15 minutes. Don't restart in a tight loop.

When not to use Weir

  • You need posts within seconds. Use X's filtered stream; Weir polls.
  • Your rule matches thousands of posts an hour. A monitor reads about 20 per run.
  • You want a cheaper hosted stream. TwitterAPI.io lists $0.15 per 1,000 tweets, and its filter rules can check every 0.05 seconds.

Weir reads public data only, can't post, and isn't licensed by X, so access can change.

FAQ

Is the Twitter streaming API free?

No. On X's pay-per-use plan, each post the filtered stream delivers costs $0.005, as of October 2026. You buy credits first, and X adds $20 of free credits after your first purchase with a saved card.

Does Tweepy StreamingClient still work?

Yes. Tweepy 4.17.0, from July 2026, still ships it with X's backoff built in. You need a bearer token and X credits. Tag every rule, and catch errors in your callbacks.

How do I get tweets I missed during a disconnect?

On pay-per-use, run X's search over the gap, because backfill and Recovery are Enterprise only. Store the newest post id you received and search from it, the same idea as Weir's sinceId.

Sources

All checked on 22 October 2026: