Source: https://cli.nylas.com/guides/gmail-api-batch-delete

# Gmail API batchDelete: Bulk Delete Messages

One batchDelete call can erase 1,000 messages in a single request, and none of them pass through the trash on the way out. That power is why the method requires Gmail's broadest OAuth scope, and why most bulk-cleanup scripts should reach for a recoverable alternative first. This guide covers the 1,000-ID limit, the scope requirement, the quota math against trash-based deletion, and the list-first patterns that keep a bad filter from destroying a mailbox.

Written by [Qasim Muhammad](https://cli.nylas.com/authors/qasim-muhammad) Staff SRE

Updated June 6, 2026

> **TL;DR:** `messages.batchDelete` permanently removes up to 1,000 message IDs per request for 50 quota units, but requires the full `https://mail.google.com/` scope and offers no undo. For recoverable cleanup, batch-apply the TRASH label with `batchModify` (also 50 units), or pipe `nylas email list --json` through `jq` and `xargs` from the terminal.

> **Disclosure:** Nylas CLI is built by Nylas, Inc. This comparison reflects our testing and product understanding as of June 6, 2026.

Command references used in this guide: [`nylas email list`](https://cli.nylas.com/docs/commands/email-list) and [`nylas email delete`](https://cli.nylas.com/docs/commands/email-delete).

## What does messages.batchDelete do?

The `users.messages.batchDelete` method permanently deletes a list of messages by ID in one HTTP request. Per the [API reference](https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.messages/batchDelete), it "deletes many messages by message ID" and "provides no guarantees that messages were not already deleted or even existed at all." This is permanent deletion, not a move to Trash, so the 30-day recovery window that trashed messages get doesn't apply.

The method authorizes against exactly one scope: `https://mail.google.com/`, Gmail's full-access scope. Narrower scopes like `gmail.modify` don't work, which has two consequences. Your OAuth consent screen asks users for complete mailbox control, and apps using this scope face Google's restricted-scope verification review. The single-message `messages.delete` shares the same constraint; its own reference says it "immediately and permanently deletes the specified message" and recommends `messages.trash` instead.

## What is the 1,000-ID limit per request?

The `ids` array in a batchDelete request caps at 1,000 message IDs. Google's [Python client documentation](https://googleapis.github.io/google-api-python-client/docs/dyn/gmail_v1.users.messages.html) describes the field as "The IDs of the messages to delete. There is a limit of 1000 ids per request." Clearing 25,000 messages therefore takes a minimum of 25 chunked requests.

The chunking pattern below collects IDs with paginated `messages.list` calls (5 quota units each), then deletes in 1,000-ID slices. The full sweep of 25,000 messages costs 1,500 quota units (250 for the 50 list calls, 1,250 for the 25 deletes), well inside the 6,000-units-per-minute per-user budget.

```python
def collect_ids(service, query):
    """Page through messages.list and return all matching IDs."""
    ids, token = [], None
    while True:
        resp = service.users().messages().list(
            userId="me", q=query, maxResults=500, pageToken=token
        ).execute()
        ids += [m["id"] for m in resp.get("messages", [])]
        token = resp.get("nextPageToken")
        if not token:
            return ids

ids = collect_ids(service, "from:noreply@example.com older_than:1y")
print(f"about to permanently delete {len(ids)} messages")

# batchDelete accepts at most 1,000 IDs per request
for i in range(0, len(ids), 1000):
    service.users().messages().batchDelete(
        userId="me", body={"ids": ids[i:i + 1000]}
    ).execute()
```

Exceeding the cap fails the whole request, so always slice client-side. The response body is empty on success, which means your script's own count of submitted IDs is the only record of what was removed.

## batchDelete vs trash vs delete: which should you use?

The Gmail API has three deletion paths, and they differ on recoverability, batch size, scope, and quota cost. Google's [usage limits page](https://developers.google.com/workspace/gmail/api/reference/quota) prices batchDelete and batchModify at 50 units each, messages.trash at 20, and messages.delete at 10. The recoverable bulk option is `batchModify` with the TRASH label, since Google's [labels guide](https://developers.google.com/workspace/gmail/api/guides/labels) lists TRASH as a system label that can be manually applied.

| Method | Recoverable | Max per request | Minimum scope | Quota units |
| --- | --- | --- | --- | --- |
| messages.batchDelete | No | 1,000 | https://mail.google.com/ | 50 |
| batchModify + TRASH label | Yes (30 days) | 1,000 | gmail.modify | 50 |
| messages.trash | Yes (30 days) | 1 | gmail.modify | 20 |
| messages.delete | No | 1 | https://mail.google.com/ | 10 |

The quota math favors batching heavily. Trashing 1,000 messages with individual `messages.trash` calls costs 20,000 units, more than 3x the 6,000-unit per-user-per-minute limit, so the loop throttles with 429 errors. One `batchModify` call covers the same 1,000 messages for 50 units: a 400x reduction. Reserve `batchDelete` for cases where permanent, immediate removal is the requirement, like purging sensitive data under a retention policy.

## How do you avoid deleting the wrong messages?

A batchDelete guardrail is a check that runs between collecting IDs and submitting the request. The method itself offers none: no dry-run flag, no undo, and an empty response body. Three checks cover most failure modes, and they cost at most a few hundred quota units on a 10,000-message sweep.

1. **Count before you delete** — print `len(ids)` and require a typed confirmation when it exceeds your expected ceiling. A query like `older_than:1y` with a typo (`older_than:1d`) changes the match set by orders of magnitude.
2. **Sample the matches** — fetch 5 random IDs with `messages.get` in `metadata` format and print their subjects and senders. A wrong-folder filter is obvious in 5 samples.
3. **Export, then delete** — write the matched messages to JSON first. The [backup guide](https://cli.nylas.com/guides/backup-emails-to-json) covers a full export pipeline you can run before any destructive pass.

Staging the cleanup as trash-first also works as a guardrail: run `batchModify` with the TRASH label today, then let a second job call `batchDelete` on the TRASH label 7 days later. Anything flagged wrongly in the meantime is one click to restore inside Gmail's 30-day window.

## How do you bulk delete from the CLI?

The `nylas email delete` command removes a message by ID across Gmail, Outlook, and 4 other providers, and deletion moves the message to the provider's trash rather than erasing it. Piping `nylas email list --json` through `jq` filters and `xargs` gives you a bulk pipeline with no Google Cloud project or restricted-scope OAuth review.

```bash
# Count matches first (guardrail #1)
nylas email list --from noreply@example.com --json --limit 200 | jq length

# Bulk delete: read messages from one sender
nylas email list --from noreply@example.com --json --limit 200 | \
  jq -r '.[] | select(.unread == false) | .id' | \
  xargs -I {} nylas email delete {} --yes

# Review what's sitting in trash afterwards
nylas email list --folder TRASH --limit 20
```

Because the CLI path trashes rather than purges, it pairs with Gmail's 30-day recovery window instead of fighting it. The [delete and archive guide](https://cli.nylas.com/guides/delete-emails-from-terminal) covers thread deletion, date-based filters, and per-provider trash retention in detail.

## Next steps

- [Delete and archive email from the CLI](https://cli.nylas.com/guides/delete-emails-from-terminal) — single-message, thread, and filtered bulk cleanup patterns
- [Gmail API: list spam and trash](https://cli.nylas.com/guides/gmail-api-spam-trash) — read the TRASH label to audit what a cleanup run moved
- [Gmail API quotas](https://cli.nylas.com/guides/gmail-api-quotas-2026) — per-method costs and the 6,000-unit per-minute budget explained
- [Back up emails to JSON](https://cli.nylas.com/guides/backup-emails-to-json) — export matched messages before a destructive pass
- [Full command reference](https://cli.nylas.com/docs/commands) — every flag and subcommand documented
