← Back to lab

Bing Had 2.3% of Google's Impressions and 36% of My Search Clicks. My SEO Automation Only Read Google.

Over 28 days Bing sent 138 clicks from 3,731 impressions vs Google's 247 from 160,253. 96% of the Bing queries never showed up in the Search Console export.

Bing Had 2.3% of Google's Impressions and 36% of My Search Clicks. My SEO Automation Only Read Google.

Model: Claude Code (Opus + Sonnet) | Date: 2026-11 | Status: shipped to 6 sites

Until this week, every automated SEO decision on one of my Czech content sites (refresh queue, prune list, content gap finder, before/after measurement) ran on Search Console data alone. The first Bing Webmaster sync showed Bing sending more than a third of the clicks.

The numbers

Same 28-day window, same site, both pulled from the APIs into the same database:

SourceClicksImpressionsCTR
Google (Search Console)247160,2530.15%
Bing Webmaster1383,7313.70%
Bing as share36% of all clicks2.3% of Google's impressions24x Google's CTR

Most of Google's 160k impressions sit on positions where nobody clicks. Bing shows the site far less often, but when it does, people click. A system that ranks problems by impressions will never look at Bing. A system that ranks by clicks was missing over a third of them.

Bing sees queries Google hides

The second surprise was in the query table. Bing's per-query stats (one year of weekly buckets) held 1,016 distinct queries. The stored Search Console query export held 552.

-- Bing queries with no exact match in the stored GSC query export
SELECT COUNT(DISTINCT b.query), SUM(b.clicks)
FROM bing_query_stat b
WHERE NOT EXISTS (SELECT 1 FROM search_query_stat s WHERE s.query = b.query);
-- 976 queries, 281 of 293 query-attributed clicks

976 of 1,016 Bing queries (96%) and 281 of 293 clicks never appear in the GSC export. Exact string matching overstates the gap a little (Czech diacritics, word order), but the direction is clear: Search Console drops long-tail queries under its privacy threshold, Bing reports them. The content gap finder had been blind to the exact queries a small site lives on.

How Bing got wired into decisions

Adding a dashboard tile was not the point. The point was to change what the automation does. The rules that went in:

// Gap finder: Bing as a second demand source
private const BING_DAYS = 90;      // Bing data is sparse, use a longer window
private const BING_WEIGHT = 3.0;   // ~2% of the impressions, ~1/3 of the clicks

$bingDemand = $topic['bing_impressions'] * 28 / self::BING_DAYS * self::BING_WEIGHT;
$demand     = $topic['impressions'] + $bingDemand;
// "well served" uses Google's position if Google knows the topic, Bing's otherwise
$position   = $hasGoogle ? $topic['best_position'] : $topic['bing_best_position'];
  • Refresh queue: every item carries evidence.bing. When Bing shows the same problem as Google, priority x1.25.
  • Prune veto: a page flagged for deletion loses the delete action if it has any Bing clicks. Google-only data could mark a page as dead while it was still earning clicks elsewhere.
  • Measurement: before/after evaluation of a content change adds Bing clicks/day, but only when both windows have Bing buckets. Older snapshots without Bing stay Google-only, so the comparison stays apples to apples.
  • Gaps: topics seen only in Bing become gap candidates, tagged source: bing instead of gsc or gsc+bing.
  • Never alone: Bing never creates a queue item by itself. It confirms, weights and vetoes.

The search_* tables stayed Google-only. Bing got its own bing_* tables, so every existing report and finder kept producing the same numbers until it was deliberately changed to read Bing.

Four API traps

1. The API key belongs to the account, not the site. I have two Bing Webmaster accounts. One key sees 5 sites, the other 10+. Which key a site needs depends on which account verified it years ago. GetUserSites?apikey=... lists what a key can see, including IsVerified. Check it before wiring a new site, or the sync returns empty data for a site the key cannot see.

2. Env var shadowing. The shell env already had BING_WEBMASTER_API_KEY for the other account. The framework loads .env.local, but real environment variables win over it. Putting the right key in .env.local under the same name would have silently used the wrong account. The fix was a site-specific name (BING_WEBMASTER_API_KEY_<SITE>).

3. `GetUrlInfo` throttles after about 10 calls per key. The response is HTTP 400 with ThrottleHost in the message. The first version counted that as an error, stopped after 3, and recorded a failed heartbeat. The monitoring would have reported a broken job every night for a rate limit.

} catch (\Throwable $e) {
    if (stripos($e->getMessage(), 'throttle') !== false) {
        $out['throttled'] = true; // rate limit, not a failure: next run continues
        break;
    }
    $out['errors']++;
    if ($out['errors'] >= 3) break; // auth/quota problem: stop hammering
}
usleep(1_500_000); // was 150 ms; 1.5 s per URL

The throttle is per key, so sites sharing an account share the budget. With 10+ sites behind one key, the per-URL index check has to be a slow rolling job (40 URLs per run, resume next run), not a sweep.

4. Some data has no API. Bing's AI Performance report (Copilot citations) and "Top Opportunity Queries" exist in the UI only, no endpoint. AI Performance exports as CSV by hand. Opportunity queries were recomputed locally: position 4-20, or position 3 or better with CTR under 5%.

Smaller notes: page and query stats come as weekly buckets, not daily, so clicks/day is clicks / (weeks * 7). Data lags 3-5 days. Page x query stats (GetPageQueryStats) for the top 60 pages add about 30 s to the nightly sync.

What shipped

One custom integration on the first site (3 commits, ~1,000 lines incl. migrations), then a generic version in the shared catalog template: bing:sync and bing:check-urls, about 1,150 lines including a 226-line feature test with a faked Bing API. It was rolled out to 5 more sites the same day. Without a key the commands skip silently, so sites without Bing verification keep working.

What I'd change

The integration took one afternoon. The blind spot existed because the dashboards were built on the first data source that worked, and every later automation inherited it. Any system that deletes, prunes or deprioritizes content should check every traffic source it can reach before acting. Next is the AI Performance CSV: if Copilot citations follow the same pattern as clicks, that is another number the automation has never seen.