Skip to content
 
 

Repository files navigation

Twitter/X Mass Unfollow: Unlimited, Rate-Aware Bulk Unfollowing From the Browser Console

A console script that mass unfollows accounts on X (Twitter) with no run limit. Paste one JavaScript file into your browser console on your own Following page: it auto-scrolls the list, unfollows at a paced 10-20 seconds, verifies every unfollow actually registered, backs off when X throttles you, and remembers its progress across page reloads. No API, no OAuth, no extension.

Project Overview

X (formerly Twitter) provides no bulk unfollow. The only supported path is clicking Following -> Unfollow -> confirm on one account at a time, inside a list that lazy-loads a few hundred entries per page load and unmounts them again as you scroll. For an account that went through an automated or aggressive following phase, that means several thousand manual interactions across a UI that actively fights sequential work. The usual escape hatch is a third-party "unfollow tool", which solves the problem by taking OAuth write access to your account — handing an unknown server permission to act as you, permanently, for a one-off cleanup.

This project is a single JavaScript file you paste into your own browser console while sitting on your own Following page. It drives exactly the same DOM a human drives: it finds the Following button, clicks it, confirms the dialog, waits, and moves on. There is no API key, no OAuth grant, no browser extension, and no server — nothing leaves the tab, and revoking it means closing the page.

The design problem is not clicking buttons; it is surviving a long run. X virtualizes the follow list, so a snapshot taken at paste time is stale within seconds. X silently ignores unfollow clicks once you exceed an undocumented daily action budget, so a naive script reports thousands of successes while changing nothing. And no realistic list finishes in one sitting. This implementation addresses all three: it rescans the live DOM every round, verifies that each unfollow actually registered before counting it, and persists handled accounts to localStorage so a reload continues instead of restarting.

How It Works

The run is a single loop with no fixed iteration count. Each round:

  1. Scan the live DOM for [data-testid="UserCell"], [data-testid="cellInnerDiv"] and pick the first cell that has a profile link, has a Following-style button, and has not already been handled. The scan reads the DOM fresh every round because the previous round's element references are already stale.
  2. Skip mutuals, if enabled — cells containing "Follows you" (or its Persian equivalent) are recorded as handled-this-session and passed over. This is a session-only mark, so turning the toggle off mid-run re-opens them.
  3. Act: scroll the button into view, click it, then wait up to 7 s for the confirmation sheet ([data-testid="confirmationSheetConfirm"], polled every 300 ms) and click it.
  4. Verify: poll the original button for up to 3 s. X flips its label from "Following" to "Follow" only when the unfollow is genuinely accepted. A button that still reads "Following" is a failed unfollow, not a successful one — this is what separates a real run from a script that cheerfully logs 3000 no-ops.
  5. Pace: sleep a random 10–20 s (configurable), counted down live in the panel, freezing while paused.
  6. Scroll when the scan finds nothing actionable, then wait for X to render the next slice. Four consecutive scrolls that grow neither the page height nor the cell count end the run.
                        +---------------------------+
                        |   scan live DOM for the   |
              +-------->|   next unhandled target   |
              |         +-------------+-------------+
              |                       |
              |              target found?
              |                       |
              |          no  +--------+--------+  yes
              |              |                 |
              |              v                 v
              |     +-----------------+   +------------------------+
              |     | scroll to load  |   | click Following        |
              |     | more + settle   |   | confirm the dialog     |
              |     +--------+--------+   +-----------+------------+
              |              |                        |
              |         grew the list?                v
              |              |             +------------------------+
              |     yes  +---+---+  no     | verify: did the button |
              +----------+       |         | flip to "Follow"?      |
              |          |       |         +-----+------------+-----+
              |          |    4 strikes          |            |
              |          |       |            success      failure
              |          |       v               |            |
              |          |   +-------+           v            v
              |          |   | done  |    wait 10-20 s   backoff 1-15 min
              |          |   +-------+           |            |
              +----------+-----------------------+------------+

Throttle handling

Consecutive verification failures are the signal that X has started ignoring the clicks. The run backs off on an escalating ladder rather than hammering a rate-limited endpoint:

Consecutive failures Behaviour
1–2 Normal 10–20 s pacing, keep going
3 Cool down 1 minute
4 Cool down 2 minutes
5 Cool down 4 minutes
6 Cool down 8 minutes
7 Cool down 15 minutes (cap)
8 Stop the run

A single success resets the counter to zero.

Cross-session memory

Every account the script attempts is written to localStorage under x-mass-unfollow:processed. On the next paste it reloads that set and skips those accounts, so clearing a large list becomes: run until throttled, reload the page, paste again, repeat over several days.

Mutuals that were merely skipped are deliberately not persisted — otherwise flipping "Skip mutuals" off later could never reach them.

Scope

Item Supported
Page https://x.com/<you>/following (also twitter.com)
Browsers Desktop Chrome, Edge, Firefox, Safari (any console with localStorage)
Mutual detection English (follows you) and Persian (شما را دنبال می‌کند)
Button detection following, unfollow, and the Persian دنبال می‌کنید
Run limit None — runs until the list is exhausted or you stop it
Skipped automatically Cells with no profile link (promoted content, separators)
Not supported Mobile browsers, follower-side pruning, blocking, allow-lists

Quick Start

  1. Open https://x.com/<yourusername>/following.
  2. Open the developer console.
  3. Paste the entire contents of unfollow.js.
  4. Press Enter, then leave the tab open and awake. The script scrolls and loads more profiles on its own.
Chrome / Edge
  • F12, or Ctrl+Shift+J (Windows/Linux), or Cmd+Option+J (macOS)
  • First paste into the console may require typing allow pasting and pressing Enter
Firefox
  • F12, or Ctrl+Shift+K (Windows/Linux), or Cmd+Option+K (macOS)
  • Firefox asks you to type allow pasting before the first paste
Safari
  • Enable Settings -> Advanced -> Show features for web developers
  • Open the console with Cmd+Option+C
Building from the TypeScript source instead
npm install
npm run build     # tsc -> dist/unfollow.js
npm test          # jest, 35 tests
npm run lint      # tsc --noEmit

src/unfollow.ts is the typed, unit-tested source. It only auto-runs when location.hostname matches x.com or twitter.com, so importing it in tests or a bundler never starts a run. unfollow.js at the repo root is the standalone console build and runs immediately on paste.

Usage

A floating panel appears in the top-right corner. It is the whole interface — there is nothing to configure before pasting.

Control Effect
Min / Max Delay (sec) Pacing window, applied to the next wait. Defaults to 10 and 20
Skip mutuals On by default: accounts that follow you back are left alone. Uncheck to clear everyone, including mutuals
Pause / Resume Freezes the countdown where it stands; the remaining wait is not consumed while paused
Stop Ends the run after the current account and prints the report
Close (x) Stops the run and removes the panel

Live statistics: Unfollowed, Skipped, Per Hour (measured, not projected), and elapsed time. The console prints a colour-coded line per account and a console.table report at the end.

Clearing a large list

The script does not fight X's daily limit — it detects it and stops wasting clicks. For a multi-thousand list the workflow is:

paste --> run until throttled --> reload page --> paste again --> repeat

Handled accounts are remembered between rounds, so each session starts where the last one ended.

Following count Tab time at default pacing Realistic plan
500 ~2.5 hours One sitting
1,000 ~5 hours One or two sittings
3,000 ~14 hours Several days
5,000 ~24 hours About a week

Throughput is roughly 200 unfollows per hour at the default 10–20 s window: a 15 s average wait plus 1–4 s of clicking, confirming and verifying per account, before any throttling. X's own daily action budget is undocumented, varies with account age and standing, and will usually bite well before those totals are reached in a single day.

To forget saved progress and re-walk the entire list:

localStorage.removeItem('x-mass-unfollow:processed')

Technical Details

Layout

unfollow.js              standalone console build, paste-ready, runs on load
src/unfollow.ts          typed source, same logic, guarded auto-run
__tests__/unfollow.test.ts   35 jest tests (jsdom)
jest.config.js           ts-jest + jsdom, coverage from src/
tsconfig.json            ES2020, strict, outDir dist/

Exported API (src/unfollow.ts)

Function Purpose
sleep(ms) Promise-based delay
randomDelay(min?, max?) Random pacing value; tolerates swapped bounds
txt(el) Normalised lowercase text of an element
isMutual(cell) Detects "follows you" in either supported language
getUsername(cell) Extracts the handle from the cell's profile link, ignoring status and intent links
findUnfollowButton(cell) Locates the Following/Unfollow control
formatElapsed(ms) HH:MM:SS for the panel
claim(cell, username, persist?) Marks an account handled; persist: false keeps it session-only
isClaimed(cell, username) Duplicate guard, by handle or by element identity
loadMemory() / saveMemory() Read/write the localStorage progress set

Tuning constants

Constant Default Meaning
MIN_DELAY / MAX_DELAY 10000 / 20000 ms Pacing window, live-editable in the panel
SCROLL_PAUSE 2500 ms Settle time after a scroll, twice per scroll attempt
MAX_EMPTY_SCANS 4 Fruitless scroll attempts before declaring the list exhausted
FAILURE_COOLDOWN_AFTER 3 Consecutive failures before the backoff ladder starts
MAX_CONSECUTIVE_FAILURES 8 Consecutive failures before the run gives up
STORE_KEY x-mass-unfollow:processed localStorage key holding a JSON array of handles

Duplicate handling

Accounts are tracked by handle in a Set. Cells whose handle cannot be resolved fall back to a WeakSet keyed on the element itself, which avoids both re-processing and the memory leak of holding unmounted nodes. An account is claimed before the click is attempted, so a hang or an exception can never trap the loop on the same cell.

Tests

npm test

35 jsdom tests cover the pure helpers: text normalisation, mutual detection, username extraction across status/intent/plain links, button discovery in multi-button cells, the pacing window including swapped bounds, elapsed formatting, and the claim/persistence rules — including the guarantee that a skipped mutual is not written to localStorage.

Roadmap

  • Selector resilience: X ships DOM changes without notice, and data-testid values are the only stable-ish anchor. A self-check that aborts loudly when the expected structure is missing would beat silent no-ops.
  • Allow-list support, so specific handles survive a full clear.
  • Follower-side pruning (removing followers) using the same paced, verified loop.
  • Optional export of the session log as CSV for record-keeping.
  • Mobile-browser console support, which currently has no practical entry point.

Limitations and Disclaimer

This script automates the public web interface of a site you are logged into, on your own account, using your own session. It is not affiliated with X Corp. and it is not a supported integration.

Known limitations, stated plainly:

  • The tab must stay open and awake. Backgrounded tabs get their timers throttled by the browser; a sleeping laptop stops the run.
  • UI changes break selectors. When X restructures the follow list, detection fails and the run ends early rather than doing damage.
  • Mutual detection is language-dependent. Only English and Persian strings are recognised. On any other interface language, "Skip mutuals" will not reliably protect mutuals.
  • Unfollowing at volume is visible behaviour. Automation of this kind may violate X's terms of service, and aggressive use can attract rate limits or account action. The pacing and backoff exist to keep the run conservative, not to hide it.

Use it on your own account, at your own risk.

License and Attribution

MIT. Original project and copyright: Shayan Taherkhani (shayantaherkhani.ir, @shayanthn) — see LICENSE.

This fork rewrites the execution model: the fixed 190-per-run cap is replaced by an unbounded auto-scrolling loop, pacing moves to 10–20 s, unfollows are verified rather than assumed, throttling triggers an escalating backoff, and progress persists across page reloads.

About

Twitter/X mass unfollow console script — unlimited paced unfollowing with rate-limit backoff and resume

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages