Build a Local Tweet-Deletion Script: From Archive Parsing to a Resumable Batch Runner
There are two off-the-shelf ways to delete tweets and neither fits well. Hosted services want your account access. The official interface falls over somewhere in the low thousands.
The third option is a script you run yourself. It sounds like a project, but it breaks into four pieces: turn the archive into a list, obtain a credential with the right scope, call the deletion endpoint in batches, and persist progress to a file. Assemble those and you have a resumable deleter.
What follows walks through the four pieces in order, flags the decisions inside each, and points at the places where rate limits and retries tend to go wrong.
When writing one is worth it
Rule out the cases that do not need code first. A few hundred tweets are an hour or two of clicking, and writing a script costs more than that. A handful of high-risk items is also faster by hand.
Three situations justify the effort: the backlog runs into five figures and manual work is out of the question; the credential question is serious enough that you refuse to let a third party hold anything that can sign in as you; or you plan to run the same logic repeatedly, say once a quarter, and the one-time cost amortises.
| Approach | Where the credential lives | Volume it suits | Resumable |
|---|---|---|---|
| Official interface, by hand | Not involved | Up to a few hundred | You keep track yourself |
| Hosted deletion service | Third party holds it | Thousands to tens of thousands | Handled by the vendor |
| Local script | Only on your machine | Tens of thousands and up | You build it |
That last cell in the bottom row is where all the work is. A hosted service absorbs resumability and error handling for you. Writing your own means implementing both explicitly, otherwise a dropped connection halfway through leaves you either re-issuing calls or skipping a chunk.
Where the list comes from
Generate the list from an archive you downloaded yourself. Do not scrape the public timeline. Two reasons: scraping misses a lot, because deleted and previously protected posts do not show up, and the scraping itself leaves a trail of unusual requests on your account, which is the opposite of what you are trying to accomplish.
A few files in the archive matter here. tweets.js holds your own posts, each with an id, a timestamp and the body. like.js holds likes. direct-messages.js holds private messages. A deletion script usually touches only the first, and the others behave differently, as covered in the archive file breakdown.
The ids in the archive are strings and they exceed the safe integer range for JavaScript. Do not casually wrap them in Number(). The failure this produces is nasty because it hides: a truncated id still returns a success from the endpoint, and you have deleted a different post.
The four pieces
| Module | Job | The decision inside |
|---|---|---|
| Parser | Archive to deletion list | Which filter to apply; keep ids as strings |
| Credential | Obtain call permission | Which grant, and which scope |
| Batch runner | Call the endpoint per item | Batch size, throttling, timeouts |
| State file | Record what is done | When to write, what the idempotency key is |
The fourth piece is the one people skip. Getting the first three working is not hard. The fourth decides whether the thing survives a real network.
Step one: parse the archive into a list
The parser's goal is not to load everything into memory. It is to produce a stable deletion list and write it to disk. Once written, that list is the source of truth for every later run, so changing your filter mid-project cannot scramble the state.
- Read
tweets.jsand strip the assignment statement at the top. What remains is valid JSON. - Pull the id, creation time and body for each post. Keep the body around so you can spot-check.
- Apply your filter: earlier than a given year, containing a keyword, or falling inside a date range. Keep the filter as a parameter, not a hardcoded constant.
- Write the result to JSON or CSV, with ids treated as strings throughout.
After generating the list, sample twenty or thirty entries against your filter. Five minutes here prevents deleting the wrong things later.
Step two: the credential and its scope
Deletion calls need an authorization, and the scope of that authorization caps what the script can do. Hold to one principle: request only the scope you actually use. Read and write permissions are separate at the endpoint level, and the distinction is laid out in read versus write access.
The two common ways of handling the credential are both poor. Hardcoding a long-lived key into the script means one sync to a cloud folder or a repository and it has leaked. Pasting it in by hand each run does not survive a batch job.
The workable middle ground: keep the credential in a local environment variable or the system credential store, have the script read it and never write it, and give it an expiry. Even for a personal project, keep the key in its own file with an ignore rule. Details in local key management.
Step three: batching under a rate limit
The deletion endpoint is rate limited and will reject you past the ceiling. So the batch runner needs three things: a batch size, a request interval, and backoff once you get rejected.
Do not run the batch size up to the maximum. Leave headroom. Target sixty to seventy percent of the window allowance and keep the rest for retries. A fixed interval works, but reading the remaining quota from the response headers and adjusting is better. The relevant fields are described in rate limits and deletion.
Every request needs a timeout. Without one, a single stuck connection parks the whole batch with no visible progress. Set the timeout short and let the retry path handle it, which beats freezing the run.
Should you run concurrently? For a small batch, fine. For a large one, get it working serially first, then consider two or three workers. Concurrency burns the window allowance fast and makes the retry logic considerably harder to reason about.
Step four: state and resumability
The state file is what lets the script be interrupted and resumed at any point. Write after each item, not at the end of a batch. At minimum, record four fields:
| Field | Purpose |
|---|---|
| Post id | The identifier, used as the idempotency key |
| Outcome | Succeeded, no longer exists, or failed with a reason |
| Timestamp | Detects stale state, helps with debugging |
| Attempt count | Caps retries so nothing loops forever |
On startup, read the list and read the state file. The difference between them is the work for this run. The script becomes resumable by construction: rerunning repeats nothing and skips nothing.
The write timing deserves emphasis. Writing per batch, say every fifty items, loses the progress inside the batch, so an interruption re-processes a few dozen. Writing per item is slower, but for a batch job the write cost is trivial next to the network cost.
One practical detail: append to the state file rather than rewriting it. On a hard stop, an append loses at most the final line. A rewrite can leave a half-written file, and then the whole state is unreadable.
Failures: which ones to retry
Sort errors into three buckets and treat them differently:
- Retryable: rate limit responses, timeouts, connection resets. Queue with backoff, retry, increment the attempt count.
- Effectively done: the post no longer exists, or you lack permission to delete it. Not a failure. Mark it terminal, because retrying achieves nothing.
- Needs a human: authentication failure, insufficient scope. Continuing only produces more failures. Stop and check the credential.
Separating the second bucket from the third matters most. Treating an auth failure as retryable keeps the script spinning until the quota runs out. Treating an already-gone post as a failure makes it retry the same items forever. Fuller error taxonomy in deletion failure FAQ.
The security boundary of a local run
A large part of the benefit of writing your own script is that data never leaves the machine. That benefit is conditional, because the archive, the generated list, the state file and any logs all land on disk.
- Keep full post bodies out of the deletion log. Ids and outcomes are enough. Logs get synced to cloud folders without anyone deciding to.
- Put the archive and the list on the same encrypted volume, or clear them when the run finishes. See storing an archive encrypted.
- Add an ignore rule for the credential file so it does not travel with the project directory.
- Put no telemetry in the script. The value of a local tool is that it has no outbound path, with the deletion endpoint as the sole exception.
When to stop writing your own
Reaching a script that reliably completes a run usually costs a weekend. In these cases, use something off the shelf: the backlog is small; you do not intend to run this again; the account is a shared brand or corporate one where deletions need an audit trail; or the backlog is full of edge cases that demand individual judgement, so automation gains get eaten by review time.
A comparison of hosted and native options is in the 2026 tool roundup. If what you actually want is to know how much sensitive material is sitting in the account and whether it is worth cleaning, you do not need a script at all. One audit answers that.
About digital-footprint-health.shop
digital-footprint-health.shop covers the step before the script: mapping where personal data sits in the account before you write code or delete anything. The tool parses your X archive on your own device, flags phone numbers, emails and addresses with the years they appear, and produces a 0-100 health score plus a work list. The analysis is read-only and nothing is uploaded. Once the list is confirmed, deletion can run per tweet with pause and resume, or you can build it yourself along the lines above. Start with the free audit, and see downloading your X archive for the import step.
Frequently Asked Questions
How does the per-tweet cost compare between a script and a hosted service?
A script costs time, not money per tweet, so the per-item cost falls as the backlog grows. Hosted services charge per tweet and save you the development and maintenance work. The rough crossover sits in the low thousands. Below that, a service wins. Past ten thousand, with repeat runs expected, the script does.
What happens if the network drops mid-run?
It depends on when the state file is written. If you write after each item, rerunning picks up at the break point with no repeats and no gaps. If you write per batch, the remainder of that batch gets reprocessed; the repeats usually come back as already deleted and land in the done bucket, so nothing actually breaks.
Does concurrency make it much faster?
Less than you would expect. The window allowance is shared, so concurrency drains it faster, and the timing of backoff and retries becomes harder to reason about. For a large backlog, get the serial version working with reliable state writes first, then try two or three workers with a correspondingly smaller batch size.
Why keep archive post ids as strings?
Post ids exceed the range JavaScript can represent exactly as integers. Converting to a number drops the trailing digits and yields a different but still valid id. Worse, the endpoint does not error; it happily deletes a different post and reports success. Passing ids as strings end to end sidesteps the whole problem.
Does the script need write scope, or is read enough?
Deletion is a write operation, so read scope will not cover it. But nothing else in the pipeline needs write access. Parsing the archive is local and touches no endpoint, and verifying results does not either. Keep the credential narrowed to what deletion requires rather than requesting a broader set for convenience.
Check your own X/Twitter footprint
Free on-device scan. Your archive never leaves your computer.
Start Free CheckRelated Reads
Why X Rate Limits Slow Down Bulk Deletion: Quotas and Queueing
Bulk deletion is slow for one reason: how write quotas are counted inside a time window, not network speed or tool quality. Once you understand window length, per-endpoint quotas and what a 429 actually means, deletion becomes a controllable queue instead of one long sprint that restarts from zero.
Parsing a 200MB X Archive in the Browser: Streaming, Memory and Threads
No Node install, no upload, just the browser: can it parse a 200MB X archive? Yes, provided you work around three hard limits: main-thread blocking, peak memory, and how the file gets read. Here is what causes each one and how to engineer around it.
No-Code: Pull Phone Numbers Out of tweets.js
tweets.js is the heart of your X archive, every one of your tens of thousands of tweets lives there. To find every tweet containing a phone number, you do not need to learn to code. This post explains how, then gives two practical methods.