Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Download large datasets from NCBI efficiently using history server, batching, and rate limiting. Use when performing bulk sequence downloads, handling large query results, or production-scale data retrieval.
.claude/skills/bio-batch-downloads/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 57% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 210% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 75% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 207% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 116% | 0% |
<!--
#
#
-->
Download large numbers of records from NCBI efficiently using the history server, batching, and proper rate limiting.
pythonfrom Bio import Entrez import time Entrez.email = 'your.email@example.com' # Required by NCBI Entrez.api_key = 'your_api_key' # Recommended for large downloads
| Authentication | Requests/Second | Delay Between | |---------------|-----------------|---------------| | Email only | 3 | 0.34 seconds | | Email + API key | 10 | 0.1 seconds |
Get an API key at: https://www.ncbi.nlm.nih.gov/account/settings/
The history server stores search results on NCBI servers, enabling efficient batch retrieval without re-sending large ID lists.
usehistory='y'WebEnv (session ID) and query_key (result set ID)python# Search with history handle = Entrez.esearch(db='nucleotide', term='human[orgn] AND mRNA[fkey]', usehistory='y') search = Entrez.read(handle) handle.close() webenv = search['WebEnv'] query_key = search['QueryKey'] total = int(search['Count']) print(f"Found {total} records, stored in history")
pythonfrom Bio import Entrez, SeqIO import time Entrez.email = 'your.email@example.com' def batch_download(db, term, output_file, rettype='fasta', batch_size=500): # Search with history handle = Entrez.esearch(db=db, term=term, usehistory='y') search = Entrez.read(handle) handle.close() webenv = search['WebEnv'] query_key = search['QueryKey'] total = int(search['Count']) print(f"Downloading {total} records...") with open(output_file, 'w') as out: for start in range(0, total, batch_size): print(f" Fetching {start+1}-{min(start+batch_size, total)}...") handle = Entrez.efetch( db=db, rettype=rettype, retmode='text', retstart=start, retmax=batch_size, webenv=webenv, query_key=query_key ) out.write(handle.read()) handle.close() time.sleep(0.34) # Rate limiting (no API key) print(f"Saved to {output_file}")
pythonfrom Bio import Entrez import time Entrez.email = 'your.email@example.com' Entrez.api_key = 'your_api_key' # Optional def download_search_results(db, term, output_file, rettype='fasta', batch_size=500): # Search with history server handle = Entrez.esearch(db=db, term=term, usehistory='y', retmax=0) search = Entrez.read(handle) handle.close() webenv = search['WebEnv'] query_key = search['QueryKey'] total = int(search['Count']) if total == 0: print("No records found") return delay = 0.1 if Entrez.api_key else 0.34 with open(output_file, 'w') as out: for start in range(0, total, batch_size): end = min(start + batch_size, total) print(f"Downloading {start+1}-{end} of {total}") attempts = 3 for attempt in range(attempts): try: handle = Entrez.efetch(db=db, rettype=rettype, retmode='text', retstart=start, retmax=batch_size, webenv=webenv, query_key=query_key) out.write(handle.read()) handle.close() break except Exception as e: if attempt < attempts - 1: print(f" Retry {attempt+1}: {e}") time.sleep(5) else: raise time.sleep(delay) print(f"Downloaded {total} records to {output_file}") download_search_results('nucleotide', 'human[orgn] AND insulin[gene] AND mRNA[fkey]', 'insulin_mrna.fasta')
pythondef download_by_ids(db, ids, output_file, rettype='fasta', batch_size=200): total = len(ids) delay = 0.1 if Entrez.api_key else 0.34 with open(output_file, 'w') as out: for start in range(0, total, batch_size): batch = ids[start:start+batch_size] print(f"Downloading {start+1}-{start+len(batch)} of {total}") handle = Entrez.efetch(db=db, id=','.join(batch), rettype=rettype, retmode='text') out.write(handle.read()) handle.close() time.sleep(delay) print(f"Downloaded {total} records to {output_file}") # Example with list of IDs ids = ['NM_007294', 'NM_000059', 'NM_000546', 'NM_001126112', 'NM_004985'] download_by_ids('nucleotide', ids, 'genes.fasta')
For very large ID lists, post them to the history server first:
pythondef post_and_download(db, ids, output_file, rettype='fasta', batch_size=500): # Post IDs to history server handle = Entrez.epost(db=db, id=','.join(ids)) result = Entrez.read(handle) handle.close() webenv = result['WebEnv'] query_key = result['QueryKey'] total = len(ids) delay = 0.1 if Entrez.api_key else 0.34 with open(output_file, 'w') as out: for start in range(0, total, batch_size): end = min(start + batch_size, total) print(f"Fetching {start+1}-{end} of {total}") handle = Entrez.efetch(db=db, rettype=rettype, retmode='text', retstart=start, retmax=batch_size, webenv=webenv, query_key=query_key) out.write(handle.read()) handle.close() time.sleep(delay) print(f"Downloaded {total} records")
pythonfrom Bio import Entrez, SeqIO from io import StringIO import time def download_genbank_records(term, output_file, batch_size=100): Entrez.email = 'your.email@example.com' # Search handle = Entrez.esearch(db='nucleotide', term=term, usehistory='y') search = Entrez.read(handle) handle.close() webenv, query_key = search['WebEnv'], search['QueryKey'] total = int(search['Count']) records = [] for start in range(0, total, batch_size): print(f"Fetching {start+1}-{min(start+batch_size, total)} of {total}") handle = Entrez.efetch(db='nucleotide', rettype='gb', retmode='text', retstart=start, retmax=batch_size, webenv=webenv, query_key=query_key) batch_records = list(SeqIO.parse(handle, 'genbank')) handle.close() records.extend(batch_records) time.sleep(0.34) SeqIO.write(records, output_file, 'genbank') print(f"Saved {len(records)} GenBank records") return records
pythonimport time from urllib.error import HTTPError def robust_download(db, term, output_file, rettype='fasta', batch_size=500, max_retries=3): handle = Entrez.esearch(db=db, term=term, usehistory='y') search = Entrez.read(handle) handle.close() webenv, query_key = search['WebEnv'], search['QueryKey'] total = int(search['Count']) delay = 0.1 if Entrez.api_key else 0.34 with open(output_file, 'w') as out: for start in range(0, total, batch_size): for retry in range(max_retries): try: handle = Entrez.efetch(db=db, rettype=rettype, retmode='text', retstart=start, retmax=batch_size, webenv=webenv, query_key=query_key) data = handle.read() handle.close() if data.strip(): out.write(data) break except HTTPError as e: if e.code == 429: # Rate limit wait = 10 * (retry + 1) print(f"Rate limited, waiting {wait}s...") time.sleep(wait) elif retry == max_retries - 1: raise else: time.sleep(5) time.sleep(delay) print(f"Downloaded to {output_file}")
pythondef stream_download(db, term, output_file, rettype='fasta', batch_size=1000): handle = Entrez.esearch(db=db, term=term, usehistory='y') search = Entrez.read(handle) handle.close() webenv, query_key = search['WebEnv'], search['QueryKey'] total = int(search['Count']) downloaded = 0 with open(output_file, 'w') as out: for start in range(0, total, batch_size): handle = Entrez.efetch(db=db, rettype=rettype, retmode='text', retstart=start, retmax=batch_size, webenv=webenv, query_key=query_key) # Stream chunks to file while True: chunk = handle.read(8192) if not chunk: break out.write(chunk) handle.close() downloaded = min(start + batch_size, total) print(f"Progress: {downloaded}/{total} ({100*downloaded/total:.1f}%)") time.sleep(0.34)
| Database | rettype | Recommended Batch | |----------|---------|-------------------| | nucleotide | fasta | 500-1000 | | nucleotide | gb | 100-200 | | protein | fasta | 500-1000 | | protein | gp | 100-200 | | pubmed | abstract | 1000-2000 | | pubmed | xml | 200-500 |
Smaller batches for GenBank/XML (more data per record).
| Error | Cause | Solution | |-------|-------|----------| | HTTPError 429 | Rate limit exceeded | Increase delay, use API key | | HTTPError 400 | Invalid WebEnv/query_key | Session expired, re-search | | Incomplete data | Connection interrupted | Add retry logic | | Memory error | Batch too large | Reduce batch_size | | Empty response | No more records | Check total vs start |
Need to download many NCBI records?
├── Have search query?
│ └── Use esearch with usehistory='y', then batch efetch
├── Have list of IDs?
│ ├── < 200 IDs? → Direct efetch with comma-separated IDs
│ └── >= 200 IDs? → Use epost, then batch efetch
├── Need records as Biopython objects?
│ └── Parse each batch with SeqIO
├── Downloading > 10,000 records?
│ └── Use streaming to avoid memory issues
└── Getting rate limited?
└── Get API key, add retry logic<!-- AUTHOR_SIGNATURE: 9a7f3c2e-MD-BABU-MIA-2026-MSSM-SECURE -->
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 17,587 | 9,809 | -44% | 1 | 1 | 0% | 3,447 | 5,420 | +57% | 0 | 0 | — |
case-10 | pass→pass | 15,235 | 12,144 | -20% | 1 | 1 | 0% | 3,294 | 6,077 | +84% | 0 | 0 | — |
case-11 | pass→pass | 14,213 | 10,237 | -28% | 1 | 1 | 0% | 2,565 | 5,356 | +109% | 0 | 0 | — |
case-17 | pass→pass | 9,793 | 9,904 | +1% | 1 | 1 | 0% | 2,014 | 5,534 | +175% | 0 | 0 | — |
case-18 | fail→pass | 6,772 | 2,670 | -61% | 1 | 1 | 0% | 1,236 | 3,828 | +210% | 0 | 0 | — |
case-02 | pass→pass | 9,480 | 3,225 | -66% | 1 | 1 | 0% | 1,761 | 3,963 | +125% | 0 | 0 | — |
case-03 | pass→pass | 11,304 | 4,531 | -60% | 1 | 1 | 0% | 2,036 | 4,184 | +106% | 0 | 0 | — |
case-04 | pass→pass | 13,817 | 7,108 | -49% | 1 | 1 | 0% | 2,377 | 4,523 | +90% | 0 | 0 | — |
case-05 | pass→pass | 14,343 | 8,859 | -38% | 1 | 1 | 0% | 2,653 | 5,147 | +94% | 0 | 0 | — |
case-06 | fail→pass | 12,961 | 5,221 | -60% | 1 | 1 | 0% | 2,522 | 4,415 | +75% | 0 | 0 | — |
case-07 | fail→pass | 20,430 | 5,415 | -73% | 1 | 1 | 0% | 1,451 | 4,457 | +207% | 0 | 0 | — |
case-08 | pass→pass | 8,397 | 4,123 | -51% | 1 | 1 | 0% | 1,526 | 4,098 | +169% | 0 | 0 | — |
case-09 | pass→pass | 17,012 | 14,625 | -14% | 1 | 1 | 0% | 3,678 | 6,499 | +77% | 0 | 0 | — |
case-12 | pass→pass | 11,591 | 7,855 | -32% | 1 | 1 | 0% | 2,333 | 4,520 | +94% | 0 | 0 | — |
case-13 | pass→pass | 12,357 | 8,204 | -34% | 1 | 1 | 0% | 2,659 | 5,134 | +93% | 0 | 0 | — |
case-14 | pass→pass | 9,467 | 6,891 | -27% | 1 | 1 | 0% | 1,838 | 4,797 | +161% | 0 | 0 | — |
case-15 | pass→pass | 12,315 | 8,956 | -27% | 1 | 1 | 0% | 2,676 | 5,151 | +92% | 0 | 0 | — |
case-16 | pass→pass | 10,712 | 7,393 | -31% | 1 | 1 | 0% | 2,325 | 4,788 | +106% | 0 | 0 | — |
case-19 | pass→pass | 12,388 | 6,862 | -45% | 1 | 1 | 0% | 2,466 | 4,551 | +85% | 0 | 0 | — |
case-20 | fail→pass | 14,239 | 10,049 | -29% | 1 | 1 | 0% | 2,405 | 5,204 | +116% | 0 | 0 | — |
case-21 | pass→pass | 13,533 | 6,878 | -49% | 1 | 1 | 0% | 1,094 | 4,665 | +326% | 0 | 0 | — |
case-22 | pass→pass | 8,977 | 5,816 | -35% | 1 | 1 | 0% | 1,716 | 4,512 | +163% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +23 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 7/24/2026 | +32% |
Other measured skills in the registry, with their headline benchmark lift.