AnswerLine Start free

, Rank Tracking · Engineering · SERP API

Enterprise rank tracking: millions of keywords, reliably

Enterprise rank tracking is a scheduling and data-engineering problem more than a scraping one. At hundreds of thousands to millions of keywords, the questions become: how do you spread collection so it finishes on time, how do you avoid paying twice for the same SERP, how do you keep a queue flowing without overflowing it, where do billions of rows live, and how do you know a run is correct before customers see it. The sections below answer each on this API’s async tasks, with formulas to size your own system from your own measurements.

The shape of the workload

Write the matrix down first:

checks per month = keywords × markets × runs per month

Every multiplier is a product decision. A platform with 400,000 customer keywords, an average of 3 markets, checked daily, needs 400,000 × 3 × 30 = 36 million checks a month. City and device targeting on Google Search are each one more multiplier. The same platform checking the long tail weekly might need a third of that. The biggest savings come before any engineering.

Step 1: tier the schedule

Not every keyword deserves a daily check.

Tier Typical members Frequency Depth
Hot Revenue keywords, active campaigns, keywords that moved last run Daily Page 1, AI Overview on
Core Tracked keywords with traffic 2–3 times a week Page 1
Tail Long-tail and discovery keywords Weekly Page 1
Deep Competitive research needing positions 11–100 Monthly Up to 10 pages

Promote and demote automatically: a tail keyword whose position changed by more than a threshold moves to hot for a week. This keeps coverage where change happens.

Spread runs over the day and week

Do not submit a week’s work at midnight on Monday. Assign each keyword a stable slot:

import hashlib

def slot(keyword_id: str, slots: int) -> int:
    return int(hashlib.sha256(keyword_id.encode()).hexdigest(), 16) % slots

# weekly tier: 7 day-slots; daily tier: 24 hour-slots
day = slot("kw-839201", 7)
hour = slot("kw-839201", 24)

A stable hash keeps each keyword on the same day and hour, so its history has even spacing, and load is flat.

Step 2: collect each SERP once

Platforms with many customers track the same queries. A collection key identifies a unique SERP observation:

(normalized query, country, pages, include flags, date)

Normalize the query (trim, collapse whitespace, lowercase if your product treats case as equal) and deduplicate on the collection key before submission. One task serves every project that tracks that key; results are fanned out by join, not by extra requests.

-- tracked keywords across all tenants for today's slot
select md5(concat_ws('|', lower(trim(query)), country,
                     pages::text, include_aio::text, current_date::text)) as collection_key,
       min(query) as query, country, pages, include_aio,
       array_agg(project_keyword_id) as subscribers
from tracked_keywords
where schedule_day = extract(isodow from current_date) - 1
group by 1, 3, 4, 5;

The collection key doubles as the basis of the idempotency key.

Step 3: submit through a queue feeder

The limits that shape submission

From the async and rate limits docs:

Feed, don’t dump

A feeder keeps the queue at a working depth and never overflows it:

import os
import time

import requests

API = "https://api.answerline.dev"
S = requests.Session()
S.headers["Authorization"] = f"Bearer {os.environ['ANSWERLINE_API_KEY']}"
TARGET_QUEUED = 20_000   # your choice: enough buffer, far below capacity

def queued() -> int:
    return S.get(f"{API}/v1/async/status", timeout=30).json()["queuedTasks"]

def feed(pending: "Iterator[list[dict]]", record) -> None:
    for batch in pending:                     # batches of <= 500 tasks, highest priority first
        while queued() > TARGET_QUEUED:
            time.sleep(15)
        resp = S.post(f"{API}/v1/async/task/batch", json=batch, timeout=60)
        if resp.status_code == 429:
            wait = int(resp.headers.get("Retry-After", 5))
            time.sleep(wait)
            resp = S.post(f"{API}/v1/async/task/batch", json=batch, timeout=60)  # same keys: safe to resend
        resp.raise_for_status()
        record(batch, resp.json()["results"])

In production, wrap the retry in the fuller policy from API errors and retries, and read details.remainingCapacity from a QUEUE_LIMIT_EXCEEDED response to size the next batch.

Idempotency keys

Build keys from the collection key and run:

def idem_key(collection_key: str, attempt: int = 0) -> str:
    return f"serp:{collection_key}" + (f":r{attempt}" if attempt else "")

The collection key already includes the date, so re-running a crashed feeder re-submits nothing that was admitted: those items return RESOURCE_ALREADY_EXISTS. Only deliberate resubmissions of failed tasks get a suffix.

Priorities

Use a small number of levels with clear meaning, for example 8 for customer-triggered refreshes, 5 for hot, 3 for core, 1 for tail and deep. Priorities only order your own queue, so a large backfill at priority 1 never delays a customer refresh at 8.

Step 4: size throughput from your own measurements

There is no concurrency cap, so daily capacity is what your queue drains:

tasks per day ≈ measured completions per hour × 24

Measure it, do not assume it, together with D, your mean seconds per task from first start to completion: every completed task reports task.latencyMs (first start to final outcome, excluding queue time), and monitor responses carry X-Latency-Ms. Duration depends on engine, pages, AI Overview and market, so measure per task profile.

Then plan:

  1. Compute daily checks from the tiered schedule.
  2. Divide by capacity from the formula. Keep utilization well below 100% to leave room for retries and slow days.
  3. If a day does not fit, move tiers, not deadlines: shift core to fewer runs or tail to biweekly, or spread submissions more evenly (limits are on /docs/rate-limits).

The 4-hour start bound is irrelevant when a run fits your measured throughput. If you find tasks approaching it, the schedule does not fit your measured throughput.

Step 5: collect results

At this volume, polling task by task is wasteful. Use webhooks:

  1. Add webhook: {"url": ...} to every task.
  2. Run several receiver instances behind a load balancer. Each verifies Webhook-Signature on the raw body, writes the body to durable storage keyed by task.id, and answers 2xx within 15 seconds.
  3. Process asynchronously from that storage.
  4. Reconcile: for tasks whose results have not arrived after their expected finish, fetch GET /v1/async/task/{taskId}.

Webhook receiver design covers the receiver; SERP API reliability at scale covers the ledger that makes reconciliation cheap.

Step 6: storage layout

Two layers

  1. Raw layer. The full webhook body per task, compressed, in object storage, path-partitioned by date: raw/google/2026/09/17/<task_id>.json.gz. Keep the request payload beside it. The raw layer lets you re-parse history when you add a field or fix an extraction bug, without paying to collect again.
  2. Modeled layer. Extracted rows in a columnar warehouse:
Table Grain Key columns
serp_observations One per task collection_key, observed_date, query, country, has_aio, organic_count, task_id, usd_charged
serp_organic One per organic result collection_key, observed_date, position, page, domain, url, title
serp_features One per feature instance collection_key, observed_date, feature (paa, local, ads, aio_source, …), position, domain, text
project_rankings One per subscriber per observation project_keyword_id, observed_date, best_position, best_url, in_aio_sources

Partition by observed_date, cluster by country and collection_key (or domain for competitor queries). SERP data in BigQuery has DDL and load code for one warehouse.

Volume estimate

Organic rows dominate: about 10 per page per observation. 36 million observations a month at one page is roughly 360 million organic rows a month, plus features. Estimate from your own observed average row counts and bytes, and set retention per layer: modeled rows for as long as customers chart history, raw bodies for as long as you might re-parse.

Positions: define them once

Decide and document whether “position” counts only organic results (as organicResults[].position does) or includes features. Store organic position as collected and compute any “absolute” position in a view, so the definition can change without rewriting history.

Step 7: data quality before customers see it

At millions of rows, a parsing change or a misconfigured market shows up as millions of false movements. Gate publication on checks:

  1. Completeness. Share of planned collection keys with a stored, valid observation. Publish per tier only above your threshold; show gaps as gaps, never interpolate silently.
  2. Structural validity. Organic count within the keyword’s historical range, positions increasing, links present.
  3. Distribution stability. Per market, compare today’s median organic count, AI Overview rate, local pack rate and ads rate with the trailing week. A shift across thousands of keywords at once is a pipeline signal until proven otherwise.
  4. Canaries. A fixed set of keywords with well-understood SERPs in every market and slot.
  5. Movement sanity. Share of keywords whose best position changed by more than N compared with normal days. Hold the run for review when it spikes.
  6. Target assertions. The stored payload’s country, pages and include flags match the plan.
  7. Duplicates. One observation per collection key per day; duplicates signal a key-construction bug.

Failed tasks are not charged, and resubmitting them under a new key suffix within the run window closes most completeness gaps. Budget resubmissions per run; a large failure share is an incident to alert on, not something to retry through.

Monthly cost by program

Prices at the Starter tier: Google Search $0.00103 for one page; +$0.00071 per extra page; +$0.00071 for include.aioverview. The price is the same on every tier ($1.03 per 1,000 one-page tasks).

Program Calculation Per month at Starter prices
1,000,000 keywords, weekly, one market 1,000,000 × 4 × $0.00103 $4,120
400,000 keywords × 3 markets, daily 36,000,000 × $0.00103 $37,080
Same, tiered: 10% daily, 30% three times a week, 60% weekly 1,200,000 × (0.1 × 30 + 0.3 × 13 + 0.6 × 4) × $0.00103 about $11,494.80
Hot tier with AI Overview: 50,000 keywords daily 50,000 × 30 × $0.00174 $2,610
Deep tier: 20,000 keywords monthly, 10 pages 20,000 × ($0.00103 + 9 × $0.00071) $148.40

Shared collection reduces the observation count before these multipliers apply; measure your overlap rate across tenants, because on multi-tenant platforms it is often the largest single saving. Tiers and their prices are on /pricing, and cost planning walks through budgeting.

Operating it

Pitfalls

See the rank tracking use case and the async tasks docs to start.

Questions

How do I track a million keywords without overwhelming the API?

Spread the work over time and feed the queue instead of dumping it: submit batches of up to 500 tasks while GET /v1/async/status shows capacity, give time-sensitive work a higher priority, and size daily volume to the completion rate you measure.

How many rank checks can I run per day?

There is no concurrency cap and no per-account request rate limit; the async queue holds about 100,000 tasks of a typical prompt. Measure how many tasks complete per hour on your own keywords and markets (task.latencyMs shows per-task duration), multiply by 24, and keep headroom for retries.

What is shared collection in rank tracking?

Collecting each unique combination of query, market and date once and fanning the result out to every project or tenant that tracks it. For platforms with many customers tracking overlapping keywords, it removes duplicate requests before they cost anything.

How should rank tracking data for millions of keywords be stored?

Keep the raw result JSON compressed in object storage so it can be re-parsed, and load extracted rows into a columnar warehouse partitioned by date and clustered by keyword and market. Store the request payload with every result.

What does tracking a million keywords weekly cost?

A Google Search request is $0.00103 for one page at the Starter tier, so one million keywords checked four times a month is about $4,120 a month, before AI Overview (plus $0.00071 each), extra pages (plus $0.00071 each) or additional markets. A 30-day spend that size reaches the Max tier, which lowers the per-request price.

Try it on your own prompts

100 free requests on any engine, no card. One POST returns the answer, sources and citations as JSON.

Keep reading