Archive
Batch AI Processing: Why Multithreading is the Wrong Instinct
When developers first encounter a large-scale AI classification job — say, two million records that each need to be sent to an LLM for analysis — the instinct is immediately familiar: spin up threads, parallelise the work, saturate the API. It’s the same pattern that works for database processing, file I/O, HTTP scraping. More threads, more throughput.
With LLM APIs, that instinct leads you straight into a wall. And the wall has a name: TPM.
The Problem with Multithreading LLM Calls
Most LLM APIs — OpenAI included — impose a Tokens Per Minute (TPM) limit. This is a rolling window, not a per-request limit. Every token you send in a prompt, and every token the model returns, counts against it.
The naive multithreaded approach burns through this budget in a way that’s both wasteful and hard to control:
The system prompt repeats on every request. If your prompt is 700 tokens and you’re running 20 threads firing one request each, you’re spending 14,000 tokens per second just on prompt overhead — before the model has classified a single record. With a 200,000 TPM limit, you’ve consumed 4.2 minutes of budget in one second.
Burst behaviour triggers rate limits unpredictably. The TPM limit is a rolling window. Twenty threads firing simultaneously create a spike that can exceed the per-minute budget in seconds, even if your average rate would be well within limits. The API returns 429 errors, your retry logic kicks in, those retries themselves consume tokens, and the situation compounds.
Thread count is a blunt instrument. Dialling concurrency up and down doesn’t map cleanly to token consumption because request latency varies. A batch that takes 500ms doesn’t consume the same tokens as one that takes 1,500ms, but both hold a thread slot for their duration.
The Better Model: Semantic Batching
The insight that changes everything is this: the system prompt is a fixed overhead, and you should amortise it across as many classifications as possible per API call.
Instead of:
Thread 1: [system prompt 700 tokens] + [address 1: 15 tokens] → [result: 15 tokens]Thread 2: [system prompt 700 tokens] + [address 2: 15 tokens] → [result: 15 tokens]...× 20 threadsTotal: 14,000 tokens for 20 classifications
You send:
[system prompt 700 tokens] + [addresses 1-20: 300 tokens] → [results 1-20: 100 tokens]Total: 1,100 tokens for 20 classifications
That’s a 12× reduction in token consumption for the same work. Suddenly your 200,000 TPM budget — which could only sustain ~270 single-record requests per minute — supports ~3,600 classifications per minute. No extra threads needed.
Key Implementation Details
1. Include an ID in Both Request and Response
The most important correctness detail in batch processing is never rely on positional alignment.
If you send 20 addresses and ask the model to return 20 results, it might return 19. Now you don’t know which one it dropped. If you’re matching by position, records from item 7 onwards get silently misclassified.
The fix is to include a unique identifier in both directions:
User message:id=548033: product Xid=548034: product Y...System prompt format instruction:Reply ONLY with a JSON array. Format: [{"id":548033,"c":"E"}, ...]
Now you build a dictionary from the response keyed on id, and match each input item explicitly. A missing id means that specific record gets skipped and retried on the next run. Everything else classifies correctly regardless of what the model dropped.
2. Resolve Labels Locally
The model doesn’t need to return the full label text. "Prime City Professionals" costs tokens on every response item. A single letter costs one token.
Keep a static dictionary in your code:
csharp
private static readonly Dictionary<string, string> Labels = new(){ { "A", "Prime Product" }, { "B", "Budget Product" }, // ...};
The model returns "c":"A", you look up the label locally. This also eliminates a class of hallucination errors where the model invents a label name slightly different from your taxonomy.
Note: even "category" vs "c" matters at scale. In the OpenAI tokenizer, "category" is 3 tokens; "c" is 1. Across 100,000 batch calls, that’s 200,000 tokens — small but free.
3. Track TPM with a Rolling Window, Not Concurrency
Rather than trying to infer safe concurrency from trial and error, measure what you’re actually consuming and throttle directly on that signal.
csharp
// On each successful response, record tokens used with a timestamptokenWindow.Enqueue((DateTime.UtcNow, inputTokens + outputTokens));// Before each request, prune entries older than 60 seconds and sum the restvar cutoff = DateTime.UtcNow.AddSeconds(-60);while (window.Peek().t < cutoff) window.Dequeue();long tpmUsed = window.Sum(x => x.tok);// Throttle graduated to usageif (tpmUsed > tpmLimit * 0.98) Thread.Sleep(2000);else if (tpmUsed > tpmLimit * 0.95) Thread.Sleep(800);else if (tpmUsed > tpmLimit * 0.85) Thread.Sleep(300);
This gives you automatic, self-correcting throttling that responds to real consumption rather than guessing from thread counts. If a batch of records happens to have longer addresses, the window fills faster and the delay kicks in sooner. No manual tuning required.
4. Resumability via Cursor Pagination
For a job that takes hours or days, stopping and restarting must be safe and cheap. The key is two things working together:
Write results immediately after each batch, not at the end of a page. If you crash mid-page, you’ve lost one batch (20 records), not a thousand.
Use a NULL-check filter combined with cursor pagination. The query for unclassified records looks like:
sql
WHERE segment_category IS NULL AND id > {lastId} ORDER BY id LIMIT 1000
On restart, lastId resets to 0, but the IS NULL filter automatically skips everything already classified. The cursor (id > lastId) keeps the query fast on large tables — OFFSET pagination slows to a crawl at millions of rows because the database still has to scan all preceding rows to find the offset position.
5. Handle Partial Batches Gracefully with Skip vs Error
Not all failures are equal. Distinguish between:
- Error: something went wrong that warrants logging (HTTP 500, persistent 429 after retries, DB connection failure). These need attention.
- Skip: the record wasn’t returned in this batch response. Leave it NULL in the database, it will be picked up automatically on the next run. No log noise needed.
This distinction keeps your error output meaningful. If every missing batch item logs as an error, a run with 0.1% skip rate produces thousands of error lines that mask real problems.
The Result
What started as a job estimated at 16–67 days with a naive multithreaded approach settled to around 7 hours using semantic batching — processing two million records through a rate-limited API without a single configuration change to the API account.
The throughput improvement didn’t come from more concurrency. It came from being smarter about what gets sent in each request.
The general principle applies beyond LLM classification: whenever you have a fixed overhead per API call (authentication, context, schema), the correct optimisation is to amortise that overhead across as much work as possible per call, not to fire more calls in parallel.
Summary of Patterns
| Pattern | Naive approach | Better approach |
|---|---|---|
| Throughput | More threads | Larger batches |
| Rate limiting | Catch 429, retry | Track TPM rolling window, throttle proactively |
| Result matching | Positional array index | ID-keyed dictionary |
| Label resolution | Ask model for full text | Return code, resolve locally |
| Resumability | Track page offset | NULL-check filter + cursor pagination |
| Failure handling | All failures are errors | Skip vs Error distinction |
| DB resilience | Crash on connection drop | Exponential backoff retry |
The instinct to parallelise is correct in principle — you want to keep the API busy. But with token-limited LLM APIs, the right parallelism is within a single request, not across many simultaneous ones.
Fixing Chrome’s “Aw, Snap!” STATUS_ACCESS_VIOLATION in CDP Automation
How a race condition between page navigation and JavaScript execution causes STATUS_ACCESS_VIOLATION crashes — and how to fix it properly.
C# · .NET·Puppeteer / CDP·Chrome Automation 💥
😬 Aw, Snap! Something went wrong displaying this page.
Chrome’s infamous crash page — and the Windows error behind it: STATUS_ACCESS_VIOLATION (0xC0000005). If you’re automating Chrome via CDP and seeing this, a race condition is likely the culprit.
If you’ve built a browser automation system using the Chrome DevTools Protocol — whether through Puppeteer, Playwright, or a custom CDP client — you may have encountered Chrome processes dying with a STATUS_ACCESS_VIOLATION error. The browser just vanishes. No clean exception, no useful log output. Just Chrome’s “Aw, Snap!” page, or worse, a completely dead process.
This error code (0xC0000005 on Windows) means the process attempted to read or write memory it doesn’t own. It’s a hard native crash, well below the level where .NET exception handling can help you. A try/catch around your CDP call won’t save you.
The Usual Suspects
Most writeups on this error point to GPU driver issues, sandbox misconfiguration, or DLL injection from antivirus software — and those are all valid causes. The standard advice is to throw flags like --disable-gpu, --no-sandbox, and --disable-dev-shm-usage at the problem until it goes away.
But there’s another cause that gets far less attention: injecting JavaScript into a page that’s mid-navigation. This is a timing issue, and it’s surprisingly easy to introduce.
The Race Condition
Consider a common automation pattern: click a button, wait for the resulting page to load, then execute some JavaScript on the new page. A naive implementation might look like this:
problematic// Click a button that triggers navigation
await ExecuteJavascript("document.querySelector('button').click();");
// Arbitrary delay, then poll readyState
await Task.Delay(1000);
while (true)
{
await Task.Delay(500);
var readyState = await ExecuteJavascript("document.readyState");
if (readyState == "complete") break;
}
This pattern has a fundamental flaw. After clicking the button, Chrome begins tearing down the current document and loading a new one. There is a window — however brief — where the old document is gone but the new one isn’t yet attached. If ExecuteJavascript fires a CDP Runtime.evaluate command into that gap, Chrome is asked to execute JavaScript in a context that no longer exists.
The result isn’t a clean error. Chrome’s internal state becomes inconsistent, and the access violation follows.
Why does it only crash sometimes? Because the race condition is non-deterministic. On a fast machine or a fast network, the new page loads before the poll fires and everything works. On a slower day, the poll lands in the gap and Chrome crashes. This makes the bug look intermittent and hardware-dependent, when it’s actually a logic error.
What the Timeline Looks Like
Button click dispatched
CDP sends Runtime.evaluate with the click expression. Navigation begins.
⚠ Danger zone begins
Old document is being torn down. New document not yet attached to the frame.
document.readyState polled
If Runtime.evaluate fires here, Chrome has no valid document context to evaluate against.
STATUS_ACCESS_VIOLATION
Chrome dereferences a null or freed pointer. Process dies. “Aw, Snap!”
New document attached (if we were lucky)
If the poll happened to land here instead, readyState returns “loading” or “complete” and everything works fine.
The Fix: Let Chrome Tell You
The correct solution is to stop guessing when navigation is complete and instead subscribe to Chrome’s own navigation events. CDP exposes Page.loadEventFired precisely for this purpose — it fires when the new page’s load event has completed, meaning the document is fully attached and ready for JavaScript execution.
fixedprivate async Task WaitForPageLoad(ChromeSession session, int timeoutMs = 30000)
{
var tcs = new TaskCompletionSource<bool>();
session.Subscribe<LoadEventFiredEvent>(e => tcs.TrySetResult(true));
var timeoutTask = Task.Delay(timeoutMs);
var completed = await Task.WhenAny(tcs.Task, timeoutTask);
if (completed == timeoutTask)
throw new TimeoutException("Page load timed out");
}
Critically, the subscription must be set up before triggering navigation — not after. Otherwise there’s a small window where the event fires before you’re listening:
usage// Subscribe FIRST, then trigger navigation
var pageLoad = WaitForPageLoad(chromeSession);
await ExecuteJavascript("document.querySelector('button').click();");
// Now wait — Chrome will signal when it's actually ready
await pageLoad;
// Safe to execute JavaScript on the new page
await ExecuteJavascript("/* your code here */");
Why Not Just Catch the Exception?
A STATUS_ACCESS_VIOLATION is a native Windows exception originating inside the Chrome process itself. It is entirely outside the .NET runtime. Wrapping your CDP calls in try/catch does nothing — there is no managed exception to catch. The Chrome process simply dies.
Similarly, adding more Task.Delay calls doesn’t fix the race condition — it just makes it less likely to trigger on any given run, while leaving the underlying problem completely intact.
Applies to Puppeteer and Other CDP Clients Too
This issue isn’t specific to C# or any particular CDP library. The same race condition can occur in Node.js with Puppeteer, Python with pyppeteer, or any system that drives Chrome via the DevTools Protocol. Puppeteer’s page.waitForNavigation() and page.waitForLoadState() exist precisely to solve this problem — they’re wrappers around the same loadEventFired event.
If you’re rolling a custom CDP client in any language, the principle is the same: never rely on arbitrary delays or polling to determine when a page is ready for JavaScript execution. Subscribe to Page.loadEventFired or Page.frameStoppedLoading, and let Chrome do the signalling.
Summary
Root cause: JavaScript injected via CDP (Runtime.evaluate) during a page transition hits Chrome in an inconsistent internal state, causing a STATUS_ACCESS_VIOLATION native crash.
Why it’s intermittent: The race condition depends on timing — fast loads mask it, slow loads expose it.
The fix: Subscribe to Page.loadEventFiredbefore triggering navigation, and await the event before executing any JavaScript. Never use Task.Delay or document.readyState polling as a substitute for proper navigation events. Chrome DevTools Protocol · Browser Automation · STATUS_ACCESS_VIOLATION
The Hidden Cost of ORDER BY NEWID()
Fetching a random row from a table is a surprisingly common requirement — random banner ads, sample data, rotating API credentials. The instinctive solution in SQL Server is elegant-looking but conceals a serious performance trap.
-- Looks innocent. Isn't.SELECT TOP 1 * FROM LARGE_TABLE ORDER BY NEWID()
What SQL Server actually does
NEWID() generates a fresh GUID for every single row in the table. SQL Server must then sort the entire result set by those GUIDs before it can hand back the top one. On a table with a million rows you are generating a million GUIDs, sorting a million rows, and discarding 999,999 of them.
The problem: On large tables, ORDER BY NEWID() performs a full table scan and a full sort — O(n log n) work — regardless of how many rows you need. It cannot use any index for ordering.
A faster alternative: seek, don’t sort
The key insight is to convert the “random sort” into a “random seek”. If we can generate a random Id value cheaply and then let the clustered index do the work, we avoid scanning the table entirely.
DECLARE @Min INT = (SELECT MIN(Id) FROM LARGE_TABLE)DECLARE @Max INT = (SELECT MAX(Id) FROM LARGE_TABLE)SELECT TOP 1 *FROM LARGE_TABLEWHERE Id >= @Min + ABS(CHECKSUM(NEWID()) % (@Max - @Min + 1))ORDER BY Id ASC
MAX(Id) and MIN(Id) are single index seeks on the primary key. CHECKSUM(NEWID()) generates a random integer without sorting anything. The WHERE Id >= clause then performs a single index seek from that point forward, and ORDER BY Id ASC TOP 1 picks up the very next row.
The result: Two index seeks to get the range, one index seek to find the row. Constant time regardless of table size.
Performance at a glance
| Approach | Reads | Sort | Scales with table size? |
|---|---|---|---|
| ORDER BY NEWID() | Full scan | Full sort | O(n log n) |
| CHECKSUM seek | 3 index seeks | None | O(1) |
Three caveats to know
1. Id gaps cause mild bias. If rows have been deleted, gaps in the Id sequence mean rows immediately after a gap are slightly more likely to be selected. For most use cases — sampling, rotation, A/B testing — this is an acceptable trade-off.
2. Ids may not start at 1. This is why we use @Min rather than hardcoding zero. If your identity seed started at 1000, NEWID() % MAX(Id) would generate values 0–999, which would never match any row and you’d always get the first row in the table.
3. CHECKSUM can return INT_MIN. ABS(INT_MIN) overflows back to negative in SQL Server. The fix is to apply the modulo before the ABS, keeping the intermediate value safely within range.
When you don’t need randomness at all
For round-robin rotation across a fixed set of rows — such as alternating between API credentials or cookie sessions — true randomness is unnecessary overhead. A deterministic slot based on the current second is even cheaper:
-- Rotates across N accounts, one per second, no writes requiredWHERE Slot = DATEPART(SECOND, GETUTCDATE()) % TotalAccounts
This resolves to a constant integer comparison — effectively a single index seek — and scales to any number of accounts automatically. No tracking table, no writes, no contention.
The takeaway: whenever you reach for ORDER BY NEWID(), ask whether you actually need true randomness or just approximate distribution. In most production scenarios, a cheap seek beats an expensive sort by several orders of magnitude.
Enrich Your Qualtrics Surveys with Real-Time Respondent Data Using AvatarAPI
Qualtrics is excellent at capturing what respondents tell you. But what if you could automatically fill in what you already know — or can discover — the moment they enter their email address?
AvatarAPI resolves an email address into rich profile data in real time: a profile photo, full name, city, country, and the social network behind it. By embedding this lookup directly into your Qualtrics survey flow, you collect more information about each respondent without asking a single extra question.
What Data Does AvatarAPI Return?
When you pass an email address to the API, it returns the following fields — all of which can be mapped into Qualtrics Embedded Data and used anywhere in your survey:
| Field | Description |
|---|---|
Image | URL to the respondent’s profile photo |
Name | Resolved full name |
City | City of residence |
Country | Country code |
Valid | Whether the email address is real and reachable |
IsDefault | Whether the avatar is a fallback/generic image |
Source.Name | The social network the data came from |
RawData | The complete JSON payload |
Watch the Video Walkthrough
Before diving into the written steps, watch this complete tutorial — from configuring the Web Service element to rendering the avatar photo on a results page:
Step-by-Step Integration Guide
You can either follow these steps from scratch, or import the ready-made AvatarAPI.qsf template file directly into Qualtrics (see Step 8).
Step 1 — Get Your AvatarAPI Credentials
Sign up at avatarapi.com to obtain a username and password. A free demo account is available for evaluation — use the credentials demo / demo to test before going live.
The API endpoint you will call is:
https://avatarapi.com/v2/api.aspx
Step 2 — Create an Email Capture Question
In your Qualtrics survey, add a Text Entry question with a Single Line selector. This is where respondents will enter their email address.
Note the Question ID assigned to this question (e.g. QID3) — you will reference it when configuring the Web Service. You can find the QID by opening the question’s advanced options.
Tip: Add email format validation via Add Validation → Content Validation → Email to ensure the value passed to the API is always well-formed.
Step 3 — Add a Web Service Element to Your Survey Flow
Navigate to Survey Flow (the flow icon in the left sidebar). Click Add a New Element Here and choose Web Service. Position this element after the block containing your email question and before your results block.
Configure it as follows:
- URL:
https://avatarapi.com/v2/api.aspx - Method: POST
- Content-Type: application/json
Step 4 — Set the Request Body Parameters
Under Set Request Parameters, switch to Specify Body Params and add these three key-value pairs:
{ "username": "your_username", "password": "your_password", "email": "${q://QID3/ChoiceTextEntryValue}"}
The Qualtrics piped text expression ${q://QID3/ChoiceTextEntryValue} dynamically inserts whatever email the respondent typed. Replace QID3 with the actual QID of your email question if it differs.
Step 5 — Map the API Response to Embedded Data Fields
Scroll down to Map Fields from Response. Add one row for each field you want to capture. The From Response column is the JSON key returned by AvatarAPI; the To Field column is the Embedded Data variable name.
| From Response (JSON key) | To Field (Embedded Data) |
|---|---|
Image | Image |
Name | Name |
Valid | Valid |
City | City |
Country | Country |
IsDefault | IsDefault |
Source.Name | Source.Name |
RawData | RawData |
Note: Qualtrics stores these variables automatically — you don’t need to pre-declare them as Embedded Data elsewhere in the flow, though doing so in the survey flow header keeps things organised.
Step 6 — Display the Avatar Photo on a Results Page
Add a Descriptive Text / Graphic question in a block placed after the Web Service call in your flow.
In the rich-text editor, switch to the HTML source view and paste this snippet:
<img src="${e://Field/Image}" alt="Profile Picture" style="width:100px; height:100px; border-radius:50%;"/>
The expression ${e://Field/Image} inserts the profile photo URL at runtime. The border-radius: 50% gives it a circular crop for a polished appearance.
You can display other fields using the same pattern:
Name: ${e://Field/Name}City: ${e://Field/City}Country: ${e://Field/Country}Source: ${e://Field/Source.Name}
Step 7 — Test with the Demo Account
Before going live, test the integration using the demo credentials. Enter a well-known email address (such as a Gmail address you know has a Google profile photo) to verify the image and data return correctly.
After a test submission, check the Survey Data tab — all mapped fields (Image, Name, City, Country, etc.) should appear as columns alongside your standard question responses.
Rate limits & production use: The demo credentials are shared and rate-limited. Swap in your own account credentials before publishing a live survey to ensure reliable performance.
Step 8 — Import the Ready-Made QSF Template
Rather than building from scratch, you can import the AvatarAPI.qsf file directly into Qualtrics. This gives you a pre-configured survey with the email question, Web Service flow, and image display block already set up.
To import: go to Create a new project → Survey → Import a QSF file and upload AvatarAPI.qsf. Then update the Web Service credentials to your own username and password, and you’re ready to publish.
How the Survey Flow Works
Once configured, your survey flow has this simple three-part structure:
- Block — Respondent enters their email address
- Web Service — Silent POST to
avatarapi.com/v2/api.aspx; response fields mapped to Embedded Data - Block — Results page displays the avatar photo and enriched profile data
The respondent experiences a seamless survey: they enter their email on page one, the API call fires silently between pages, and they see a personalised result — including their own profile photo — on page two.
Practical Use Cases
Lead enrichment surveys — Capture a prospect’s email and automatically resolve their name, city, and country without asking. Append this data to your CRM export from Qualtrics.
Event registration flows — Display the registrant’s photo back to them as a confirmation step, increasing engagement and reducing drop-off.
Email validation checkpoints — Use the Valid flag in a branch logic condition to route respondents with unresolvable addresses to a correction screen or alternative path.
Research panels — Enrich responses with geographic signals without asking respondents to self-report location, reducing survey length and improving data quality.
Get Started
- API documentation & sign-up: avatarapi.com
- API endpoint:
https://avatarapi.com/v2/api.aspx - Demo credentials: username
demo/ passworddemo - Video tutorial: Watch on YouTube
Benchmarking reCAPTCHA v3 Solver Services: Speed vs Quality Analysis

When implementing automated systems that need to solve reCAPTCHA v3 challenges, choosing the right solver service can significantly impact both your success rate and operational costs. We conducted a comprehensive benchmark test of five popular reCAPTCHA v3 solving services to compare their performance in terms of both speed and quality scores.
The Results
We tested five major captcha solving services: CapSolver, 2Captcha, AntiCaptcha, NextCaptcha, and DeathByCaptcha. Each service was evaluated both with and without residential proxy support (using Decodo residential proxies).
Speed Performance Rankings
Fastest to Slowest (without proxy):
- CapSolver – 3,383ms (3.4 seconds)
- NextCaptcha – 6,725ms (6.7 seconds)
- DeathByCaptcha – 16,212ms (16.2 seconds)
- AntiCaptcha – 17,069ms (17.1 seconds)
- 2Captcha – 36,149ms (36.1 seconds)
With residential proxy:
- CapSolver – 5,101ms (5.1 seconds)
- NextCaptcha – 10,875ms (10.9 seconds)
- DeathByCaptcha – 10,861ms (10.9 seconds)
- 2Captcha – 25,749ms (25.7 seconds)
- AntiCaptcha – Failed (task type not supported with proxy)
Quality Score Results
Here’s where the results become particularly interesting: all services that successfully completed the challenge returned identical scores of 0.10. This uniformly low score across all providers suggests we’re observing a fundamental characteristic of how these services interact with Google’s reCAPTCHA v3 system rather than differences in solver quality.
What Do These Results Tell Us?
1. The Score Mystery
A reCAPTCHA v3 score of 0.10 is at the very bottom of Google’s scoring range (0.0-1.0), indicating that Google’s system detected these tokens as very likely originating from bots. This consistent result across all five services reveals several important insights:
Why such low scores?
- reCAPTCHA v3 uses machine learning trained on actual site traffic patterns
- Without established traffic history, the system defaults to suspicious scores
- Commercial solver services are inherently detectable by Google’s sophisticated fingerprinting
- The test environment may lack the organic traffic patterns needed for v3 to generate higher scores
As mentioned in our research, CleanTalk found that reCAPTCHA v3 often returns consistent scores in test environments without production traffic. The system needs time to “learn” what normal traffic looks like for a given site before it can effectively differentiate between humans and bots.
2. Speed is the Real Differentiator
Since all services returned the same quality score, speed becomes the primary differentiator:
CapSolver emerged as the clear winner, solving challenges in just 3.4 seconds without proxy and 5.1 seconds with proxy. This represents a 10x speed advantage over the slowest service (2Captcha at 36 seconds).
NextCaptcha came in second place with respectable times of 6.7 seconds (no proxy) and 10.9 seconds (with proxy), making it a solid middle-ground option.
DeathByCaptcha and AntiCaptcha performed similarly at around 16-17 seconds without proxy, though AntiCaptcha failed to support proxy-based solving for this captcha type.
2Captcha was significantly slower at 36 seconds without proxy, though it did improve to 25.7 seconds with proxy enabled.
3. Proxy Support Variations
Proxy support proved inconsistent across services:
- Most services handled proxies well, with CapSolver, NextCaptcha, DeathByCaptcha, and 2Captcha all successfully completing challenges through residential proxies
- AntiCaptcha failed with proxy, returning an “ERROR_TASK_NOT_SUPPORTED” error, suggesting their proxy-based reCAPTCHA v3 implementation may have limitations
- Proxy impact on speed varied: Some services (2Captcha) were faster with proxy, while others (CapSolver, NextCaptcha) were slower
4. Success Rates
All services except AntiCaptcha (with proxy) achieved 100% success rates, meaning they reliably returned valid tokens. However, the validity of a token doesn’t correlate with its quality score—all tokens were valid but all received low scores from Google.
Practical Implications
For High-Volume Operations
If you’re processing thousands of captchas daily, CapSolver’s 3-5 second solve time provides a massive throughput advantage. At scale, this speed difference translates to:
- Processing 1,000 captchas with CapSolver: ~56 minutes
- Processing 1,000 captchas with 2Captcha: ~10 hours
For Quality-Sensitive Applications
The uniform 0.10 scores reveal a hard truth: commercial reCAPTCHA v3 solvers may not produce high-quality tokens that pass strict score thresholds. If your target site requires scores above 0.5 or 0.7, these services may not be suitable regardless of which one you choose.
Cost Considerations
Since all services returned the same quality, cost-per-solve becomes the tiebreaker alongside speed:
- CapSolver: ~$1.00 per 1,000 solves
- 2Captcha: ~$2.99 per 1,000 solves
- AntiCaptcha: ~$2.00 per 1,000 solves
CapSolver offers the best speed-to-cost ratio in this comparison.
The Bigger Picture: reCAPTCHA v3 Limitations
These results illuminate a broader challenge with reCAPTCHA v3 solver services. Google’s v3 system is fundamentally different from v2:
- v2 presented challenges that could be solved by humans or AI
- v3 analyzes behavior patterns, browser fingerprints, and site-specific traffic history
Commercial solvers can generate valid tokens, but those tokens carry telltale signatures that Google’s machine learning readily identifies. The consistently low scores suggest that Google has effective detection mechanisms for solver-generated traffic.
When Might Scores Improve?
Based on research and documentation:
- Production environments with real organic traffic may see better scores
- Time – letting reCAPTCHA v3 “train” on a site for days or weeks
- Mixed traffic – solver tokens mixed with legitimate user traffic
- Residential proxies – though our test showed this alone doesn’t improve scores
Conclusions and Recommendations
If Speed Matters Most
Choose CapSolver. Its 3-5 second solve times are unmatched, and at $1 per 1,000 solves, it’s also the most cost-effective option.
If You Need Proxy Support
Avoid AntiCaptcha for proxy-based v3 solving. CapSolver, NextCaptcha, and DeathByCaptcha all handled residential proxies successfully.
If Quality Scores Matter
Reconsider using solver services entirely. The uniform 0.10 scores suggest that commercial solvers may not be suitable for sites with strict score requirements. Consider alternative approaches:
- Browser automation with real user simulation
- Residential proxy networks with actual human solvers
- Challenging whether reCAPTCHA v3 is the right solution for your use case
The Bottom Line
For raw performance in a test environment, CapSolver dominated with the fastest solve times and lowest cost. However, the universal 0.10 quality scores across all services reveal that speed and cost may be moot points if your application requires high-quality scores that pass Google’s bot detection.
The real takeaway? reCAPTCHA v3 is doing its job—it successfully identifies solver-generated tokens regardless of which service you use. If you need high scores, you’ll need more sophisticated approaches than simply purchasing tokens from commercial solving services.
This benchmark was conducted in January 2026 using production API credentials for all services. Tests were performed with both direct connections and residential proxy infrastructure. Individual results may vary based on site configuration, traffic patterns, and Google’s evolving detection systems.
Migrating Google Cloud Run to Scaleway: Bringing Your Cloud Infrastructure Back to Europe
Introduction: Why European Cloud Sovereignty Matters Now More Than Ever

In an era of increasing geopolitical tensions, data sovereignty concerns, and evolving international relations, European companies are reconsidering their dependence on US-based cloud providers. The EU’s growing emphasis on digital sovereignty, combined with uncertainties around US data access laws like the CLOUD Act and recent political developments, has made many businesses uncomfortable with storing sensitive data on American infrastructure.
For EU-based companies running containerized workloads on Google Cloud Run, there’s good news: migrating to European alternatives like Scaleway is surprisingly straightforward. This guide will walk you through the technical process of moving your Cloud Run services to Scaleway’s Serverless Containers—keeping your applications running while bringing your infrastructure back under European jurisdiction.
Why Scaleway?
Scaleway, a French cloud provider founded in 1999, offers a compelling alternative to Google Cloud Run:
- 🇪🇺 100% European: All data centers located in France, Netherlands, and Poland
- 📜 GDPR Native: Built from the ground up with European data protection in mind
- 💰 Transparent Pricing: No hidden costs, generous free tiers, and competitive rates
- 🔒 Data Sovereignty: Your data never leaves EU jurisdiction
- ⚡ Scale-to-Zero: Just like Cloud Run, pay only for actual usage
- 🌱 Environmental Leadership: Strong commitment to sustainable cloud infrastructure
Most importantly: Scaleway Serverless Containers are technically equivalent to Google Cloud Run. Both are built on Knative, meaning your containers will run identically on both platforms.
Prerequisites
Before starting, ensure you have:
- An existing Google Cloud Run service
- Windows machine with PowerShell
gcloudCLI installed and authenticated- A Scaleway account (free to create)
- Skopeo installed (we’ll cover this)
Understanding the Architecture
Both Google Cloud Run and Scaleway Serverless Containers work the same way:
- You provide a container image
- The platform runs it on-demand via HTTPS endpoints
- It scales automatically (including to zero when idle)
- You pay only for execution time
The migration process is simply:
- Copy your container image from Google’s registry to Scaleway’s registry
- Deploy it as a Scaleway Serverless Container
- Update your DNS/endpoints
No code changes required—your existing .NET, Node.js, Python, Go, or any other containerized application works as-is.
Step 1: Install Skopeo (Lightweight Docker Alternative)
Since we’re on Windows and don’t want to run full Docker Desktop, we’ll use Skopeo—a lightweight tool designed specifically for copying container images between registries.
Install via winget:
powershell
winget install RedHat.Skopeo
Or download directly from: https://github.com/containers/skopeo/releases
Why Skopeo?
- No daemon required: No background services consuming resources
- Direct registry-to-registry transfer: Images never touch your local disk
- Minimal footprint: ~50MB vs. several GB for Docker Desktop
- Perfect for CI/CD: Designed for automation and registry operations
Configure Skopeo’s Trust Policy
Skopeo requires a policy file to determine which registries to trust. Create it:
powershell
# Create the config directoryNew-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.config\containers"# Create a permissive policy that trusts all registries@"{ "default": [ { "type": "insecureAcceptAnything" } ], "transports": { "docker-daemon": { "": [{"type": "insecureAcceptAnything"}] } }}"@ | Out-File -FilePath "$env:USERPROFILE\.config\containers\policy.json" -Encoding utf8
For production environments, you might want a more restrictive policy that only trusts specific registries:
powershell
@"{ "default": [{"type": "reject"}], "transports": { "docker": { "gcr.io": [{"type": "insecureAcceptAnything"}], "europe-west2-docker.pkg.dev": [{"type": "insecureAcceptAnything"}], "rg.fr-par.scw.cloud": [{"type": "insecureAcceptAnything"}] } }}"@ | Out-File -FilePath "$env:USERPROFILE\.config\containers\policy.json" -Encoding utf8
Step 2: Find Your Cloud Run Container Image
Your Cloud Run service uses a specific container image. To find it:
Via gcloud CLI (recommended):
bash
gcloud run services describe YOUR-SERVICE-NAME \ --region=YOUR-REGION \ --project=YOUR-PROJECT \ --format='value(spec.template.spec.containers[0].image)'```This returns the full image URL, something like:```europe-west2-docker.pkg.dev/your-project/cloud-run-source-deploy/your-service@sha256:abc123...
Via Google Cloud Console:
- Navigate to Cloud Run in the console
- Click your service
- Go to the “Revisions” tab
- Look for “Container image URL”
The @sha256:... digest is important—it ensures you’re copying the exact image currently running in production.
Step 3: Set Up Scaleway Container Registry
Create a Scaleway Account
- Sign up at https://console.scaleway.com/
- Complete email verification
- Navigate to the console
Create a Container Registry Namespace
- Go to Containers → Container Registry
- Click Create namespace
- Choose a region (Paris, Amsterdam, or Warsaw)
- Important: Choose the same region where you’ll deploy your containers
- Enter a namespace name (e.g.,
my-containers,production)- Must be unique within that region
- Lowercase, numbers, and hyphens only
- Set Privacy to Private
- Click Create
Your registry URL will be: rg.fr-par.scw.cloud/your-namespace
Create API Credentials
- Click your profile → API Keys (or visit https://console.scaleway.com/iam/api-keys)
- Click Generate API Key
- Give it a name (e.g., “container-migration”)
- Save the Secret Key securely—it’s only shown once
- Note both the Access Key and Secret Key
Step 4: Copy Your Container Image
Now comes the magic—copying your container directly from Google to Scaleway without downloading it locally.
Authenticate and Copy:
powershell
# Set your Scaleway secret key as environment variable (more secure)$env:SCW_SECRET_KEY = "your-scaleway-secret-key-here"# Copy the image directly between registriesskopeo copy ` --src-creds="oauth2accesstoken:$(gcloud auth print-access-token)" ` --dest-creds="nologin:$env:SCW_SECRET_KEY" ` docker://europe-west2-docker.pkg.dev/your-project/cloud-run-source-deploy/your-service@sha256:abc123... ` docker://rg.fr-par.scw.cloud/your-namespace/your-service:latest```### What's Happening:- `--src-creds`: Authenticates with Google using your gcloud session- `--dest-creds`: Authenticates with Scaleway using your API key- Source URL: Your Google Artifact Registry image- Destination URL: Your Scaleway Container RegistryThe transfer happens directly between registries—your Windows machine just orchestrates it. Even a multi-GB container copies in minutes.### Verify the Copy:1. Go to https://console.scaleway.com/registry/namespaces2. Click your namespace3. You should see your service image listed with the `latest` tag## Step 5: Deploy to Scaleway Serverless Containers### Create a Serverless Container Namespace:1. Navigate to **Containers** → **Serverless Containers**2. Click **Create namespace**3. Choose the **same region** as your Container Registry4. Give it a name (e.g., `production-services`)5. Click **Create**### Deploy Your Container:1. Click **Create container**2. **Image source**: Select "Scaleway Container Registry"3. Choose your namespace and image4. **Configuration**: - **Port**: Set to the port your app listens on (usually 8080 for Cloud Run apps) - **Environment variables**: Copy any env vars from Cloud Run - **Resources**: - Memory: Start with what you used in Cloud Run - vCPU: 0.5-1 vCPU is typical - **Scaling**: - **Min scale**: `0` (enables scale-to-zero, just like Cloud Run) - **Max scale**: Set based on expected traffic (e.g., 10)5. Click **Deploy container**### Get Your Endpoint:After deployment (1-2 minutes), you'll receive an HTTPS endpoint:```https://your-container-namespace-xxxxx.functions.fnc.fr-par.scw.cloud
This is your public API endpoint—no API Gateway needed, SSL included for free.
Step 6: Test Your Service
powershell
# Test the endpointInvoke-WebRequest -Uri "https://your-container-url.functions.fnc.fr-par.scw.cloud/your-endpoint"
Your application should respond identically to how it did on Cloud Run.
Understanding the Cost Comparison
Google Cloud Run Pricing (Typical):
- vCPU: $0.00002400/vCPU-second
- Memory: $0.00000250/GB-second
- Requests: $0.40 per million
- Plus: API Gateway, Load Balancer, or other routing costs
Scaleway Serverless Containers:
- vCPU: €0.00001/vCPU-second (€1.00 per 100k vCPU-s)
- Memory: €0.000001/GB-second (€0.10 per 100k GB-s)
- Requests: Free (no per-request charges)
- HTTPS endpoint: Free (included)
- Free Tier: 200k vCPU-seconds + 400k GB-seconds per month
Example Calculation:
For an API handling 1 million requests/month, 200ms average response time, 1 vCPU, 2GB memory:
Google Cloud Run:
- vCPU: 1M × 0.2s × $0.000024 = $4.80
- Memory: 1M × 0.2s × 2GB × $0.0000025 = $1.00
- Requests: 1M × $0.0000004 = $0.40
- Total: ~$6.20/month
Scaleway:
- vCPU: 200k vCPU-s → Free (within free tier)
- Memory: 400k GB-s → Free (within free tier)
- Total: €0.00/month
Even beyond free tiers, Scaleway is typically 30-50% cheaper, with no surprise charges.
Key Differences to Be Aware Of
Similarities (Good News):
✅ Both use Knative under the hood ✅ Both support HTTP, HTTP/2, WebSocket, gRPC ✅ Both scale to zero automatically ✅ Both provide HTTPS endpoints ✅ Both support custom domains ✅ Both integrate with monitoring/logging
Differences:
- Cold start: Scaleway takes ~2-5 seconds (similar to Cloud Run)
- Idle timeout: Scaleway scales to zero after 15 minutes (vs. Cloud Run’s varies)
- Regions: Limited to EU (Paris, Amsterdam, Warsaw) vs. Google’s global presence
- Ecosystem: Smaller ecosystem than GCP (but rapidly growing)
When Scaleway Makes Sense:
- ✅ Your primary users/customers are in Europe
- ✅ GDPR compliance is critical
- ✅ You want to avoid US jurisdiction over your data
- ✅ You prefer transparent, predictable pricing
- ✅ You don’t need GCP-specific services (BigQuery, etc.)
When to Consider Carefully:
- ⚠️ You need global edge distribution (though you can use CDN)
- ⚠️ You’re heavily integrated with other GCP services
- ⚠️ You need GCP’s machine learning services
- ⚠️ Your customers are primarily in Asia/Americas
Additional Migration Considerations
Environment Variables and Secrets:
Scaleway offers Secret Manager integration. Copy your Cloud Run secrets:
- Go to Secret Manager in Scaleway
- Create secrets matching your Cloud Run environment variables
- Reference them in your container configuration
Custom Domains:
Both platforms support custom domains. In Scaleway:
- Go to your container settings
- Add custom domain
- Update your DNS CNAME to point to Scaleway’s endpoint
- SSL is handled automatically
Databases and Storage:
If you’re using Cloud SQL or Cloud Storage:
- Databases: Consider Scaleway’s Managed PostgreSQL/MySQL or Serverless SQL Database
- Object Storage: Scaleway Object Storage is S3-compatible
- Or: Keep using GCP services (cross-cloud is possible, but adds latency)
Monitoring and Logging:
Scaleway provides Cockpit (based on Grafana):
- Automatic logging for all Serverless Containers
- Pre-built dashboards
- Integration with alerts and metrics
- Similar to Cloud Logging/Monitoring
The Broader Picture: European Digital Sovereignty
This migration isn’t just about cost savings or technical features—it’s about control.
Why EU Companies Are Moving:
- Legal Protection: GDPR protections are stronger when data never leaves EU jurisdiction
- Political Risk: Reduces exposure to US government data requests under CLOUD Act
- Supply Chain Resilience: Diversification away from Big Tech dependency
- Supporting European Tech: Strengthens the European cloud ecosystem
- Future-Proofing: As digital sovereignty regulations increase, early movers are better positioned
The Economic Argument:
Every euro spent with European cloud providers:
- Stays in the European economy
- Supports European jobs and innovation
- Builds alternatives to US/Chinese tech dominance
- Strengthens Europe’s strategic autonomy
Conclusion: A Straightforward Path to Sovereignty
Migrating from Google Cloud Run to Scaleway Serverless Containers is technically simple—often taking just a few hours for a typical service. The containers are identical, the pricing is competitive, and the operational model is the same.
But beyond the technical benefits, there’s a strategic argument: as a European company, every infrastructure decision is a choice about where your data lives, who has access to it, and which ecosystem you’re supporting.
Scaleway (and other European cloud providers) aren’t perfect replacements for every GCP use case. But for containerized APIs and web services—which represent the majority of Cloud Run workloads—they’re absolutely production-ready alternatives that keep your infrastructure firmly within European jurisdiction.
In 2026’s geopolitical landscape, that’s not just a nice-to-have—it’s increasingly essential.
Resources
- Scaleway Serverless Containers: https://www.scaleway.com/en/serverless-containers/
- Scaleway Documentation: https://www.scaleway.com/en/docs/
- Skopeo Documentation: https://github.com/containers/skopeo
- European Cloud Providers: Research Scaleway, OVHcloud, Hetzner, and others
- EU Digital Sovereignty: European Commission digital strategy resources
Have you migrated your infrastructure back to Europe? Share your experience in the comments below.
Google Calendar Privacy Proxy
https://github.com/infiniteloopltd/Google-Calendar-Redactor-Proxy/
A lightweight Google Cloud Run service that creates privacy-protected calendar feeds from Google Calendar. Share your availability with colleagues without exposing personal appointment details.
The Problem
You want to share your calendar availability with work colleagues, but:
- You have multiple calendars (work, personal, family) that you need to consolidate
- Google Calendar’s subscribed calendars (ICS feeds) don’t count toward your Outlook free/busy status
- You don’t want to expose personal appointment details to work contacts
- Outlook’s native calendar sharing only works with Exchange/Microsoft 365 calendars, not external ICS subscriptions
This service solves that problem by creating a privacy-filtered calendar feed that Outlook can subscribe to, showing you as “Busy” during your appointments without revealing what those appointments are.
How It Works
Google Calendar → This Service → Privacy-Protected ICS Feed → Outlook
(full details) (redaction) (busy blocks only) (subscription)
The service:
- Fetches your Google Calendar ICS feed using the private URL
- Strips out all identifying information (titles, descriptions, locations, attendees)
- Replaces event summaries with “Busy”
- Preserves all timing information (when you’re busy/free)
- Returns a sanitized ICS feed that Outlook can subscribe to
Use Cases
- Multiple calendar consolidation: Combine work, personal, and family calendars into one availability view
- Privacy-protected sharing: Share when you’re busy without sharing what you’re doing
- Cross-platform calendaring: Bridge Google Calendar into Outlook environments
- Professional boundaries: Keep personal life private while showing accurate availability
Quick Start
1. Get Your Google Calendar Private URL
- Open Google Calendar
- Click the ⚙️ Settings icon → Settings
- Select your calendar from the left sidebar
- Scroll to “Integrate calendar”
- Copy the “Secret address in iCal format” URL
Your URL will look like:
https://calendar.google.com/calendar/ical/info%40infiniteloop.ie/private-xxxxxxx/basic.ics
2. Deploy the Service
# Edit deploy.bat and set your PROJECT_ID deploy.bat # Or deploy manually gcloud run deploy calendar-proxy --source . --platform managed --region europe-west1 --allow-unauthenticated
You’ll get a service URL like: https://calendar-proxy-xxxxxxxxxx-ew.a.run.app
3. Construct Your Privacy-Protected Feed URL
From your Google Calendar URL:
https://calendar.google.com/calendar/ical/info%xxxxx.xxx/private-xxxxxxx/basic.ics
Extract:
- calendarId:
info@infiniteloop.ie(URL decoded) - privateKey:
xxxxxxxxxx(just the key, without “private-” prefix)
Build your proxy URL:
https://calendar-proxy-xxxxxxxxxx-ew.a.run.app/calendar?calendarId=info@infiniteloop.ie&privateKey=xxxxxxx
4. Subscribe in Outlook
Outlook Desktop / Web
- Open Outlook
- Go to Calendar
- Click Add Calendar → Subscribe from web
- Paste your proxy URL
- Give it a name (e.g., “My Availability”)
- Click Import
Outlook will now show:
- ✅ Blocked time during your appointments
- ✅ “Busy” status for those times
- ❌ No details about what the appointments are
What Gets Redacted
The service removes all identifying information:
| Original ICS Property | Result |
|---|---|
SUMMARY: (event title) | → "Busy" |
DESCRIPTION: (event details) | → Removed |
LOCATION: (where) | → Removed |
ORGANIZER: (who created it) | → Removed |
ATTENDEE: (participants) | → Removed |
URL: (meeting links) | → Removed |
ATTACH: (attachments) | → Removed |
CLASS: (privacy) | → Set to PRIVATE |
What Gets Preserved
All timing and scheduling information remains intact:
- ✅ Event start times (
DTSTART) - ✅ Event end times (
DTEND) - ✅ Event duration
- ✅ Recurring events (
RRULE) - ✅ Exception dates (
EXDATE) - ✅ Event status (confirmed, tentative, cancelled)
- ✅ Time zones
- ✅ All-day events
- ✅ Unique identifiers (
UID)
Technical Details
Stack: .NET 8 / ASP.NET Core Minimal API
Hosting: Google Cloud Run (serverless)
Cost: Virtually free for personal use (Cloud Run free tier: 2M requests/month)
Latency: ~200-500ms per request (fetches from Google, processes, returns)
API Endpoint
GET /calendar?calendarId={id}&privateKey={key}
Parameters:
calendarId(required): Your Google Calendar ID (usually your email)privateKey(required): The private key from your Google Calendar ICS URL
Response:
- Content-Type:
text/calendar; charset=utf-8 - Body: Privacy-redacted ICS feed
Local Development
# Run locally dotnet run # Test curl "http://localhost:8080/calendar?calendarId=test@example.com&privateKey=abc123"
Deployment
Prerequisites
- Google Cloud SDK installed
- .NET 8 SDK installed
- GCP project with Cloud Run API enabled
- Billing enabled on your GCP project
Deploy
# Option 1: Use the batch file deploy.bat # Option 2: Manual deployment gcloud run deploy calendar-proxy ^ --source . ^ --platform managed ^ --region europe-west1 ^ --allow-unauthenticated ^ --memory 512Mi
The --allow-unauthenticated flag is required so that Outlook can fetch your calendar without authentication. Your calendar data is still protected by the private key in the URL.
Security & Privacy
Is This Secure?
Yes, with caveats:
✅ Your calendar data is already protected by Google’s private key mechanism
✅ No data is stored – the service is stateless and doesn’t log calendar contents
✅ HTTPS encrypted – All traffic is encrypted in transit
✅ Minimal attack surface – Simple pass-through service with redaction
⚠️ Considerations:
- Your private key is in the URL you share (same as Google’s original ICS URL)
- Anyone with your proxy URL can see your busy/free times (but not details)
- The service runs as
--allow-unauthenticatedso Outlook can fetch it - If you need stricter access control, consider adding authentication
Privacy Features
- Strips all personally identifying information
- Marks all events as
CLASS:PRIVATE - No logging of calendar contents
- No data persistence
- Stateless operation
Recommendations
- Don’t share your proxy URL publicly
- Treat it like a password – it grants access to your availability
- Regenerate your Google Calendar private key if compromised
- Monitor your Cloud Run logs for unexpected access patterns
Cost Estimation
Google Cloud Run pricing (as of 2025):
- Free tier: 2M requests/month, 360,000 GB-seconds/month
- Typical calendar: Refreshes every 30-60 minutes
- Monthly cost: $0 for personal use (well within free tier)
Even with 10 people subscribing to your calendar refreshing every 30 minutes:
- ~14,400 requests/month
- ~$0.00 cost
Troubleshooting
“404 Not Found” when subscribing in Outlook
- Verify your service is deployed:
gcloud run services list - Check your URL is correctly formatted
- Ensure
--allow-unauthenticatedis set
“Invalid calendar” error
- Verify your Google Calendar private key is correct
- Test the URL directly in a browser first
- Check that your calendarId doesn’t have URL encoding issues
Events not showing up
- Google Calendar ICS feeds can take 12-24 hours to reflect changes
- Try re-subscribing to the calendar in Outlook
- Verify the original Google Calendar ICS URL works
Deployment fails
# Ensure you're authenticated gcloud auth login # Set your project gcloud config set project YOUR_PROJECT_ID # Enable required APIs gcloud services enable run.googleapis.com gcloud services enable cloudbuild.googleapis.com
Limitations
- Refresh rate: Calendar clients typically refresh ICS feeds every 30-60 minutes (not real-time)
- Google’s ICS feed: Updates can take up to 24 hours to reflect in the ICS feed
- Authentication: No built-in authentication (relies on URL secrecy)
- Multi-calendar: Requires one proxy URL per Google Calendar
Alternatives Considered
| Solution | Pros | Cons |
|---|---|---|
| Native Outlook calendar sharing | Built-in, real-time | Only works with Exchange calendars |
| Calendly/Bookings | Professional, feature-rich | Monthly cost, overkill for simple availability |
| Manual sync (Zapier/Power Automate) | Works | Complex setup, ongoing maintenance |
| This solution | Simple, free, privacy-focused | Relies on ICS feed delays |
Contributing
Contributions welcome! Areas for enhancement:
- Add basic authentication support
- Support multiple calendars in one feed
- Caching layer to reduce Google Calendar API calls
- Health check endpoint
- Metrics/monitoring
- Custom “Busy” text per calendar
License
MIT License – free to use, modify, and distribute.
Author
Created by Infinite Loop Development Ltd to solve a real business need for calendar privacy across platforms. https://github.com/infiniteloopltd/Google-Calendar-Redactor-Proxy/
Controlling Remote Chrome Instances with C# and the Chrome DevTools Protocol
If you’ve ever needed to programmatically interact with a Chrome browser running on a remote server—whether for web scraping, automated testing, or debugging—you’ve probably discovered that it’s not as straightforward as it might seem. In this post, I’ll walk you through how to connect to a remote Chrome instance using C# and the Chrome DevTools Protocol (CDP), with a practical example of retrieving all cookies, including those pesky HttpOnly cookies that JavaScript can’t touch.
Why Remote Chrome Control?
There are several scenarios where controlling a remote Chrome instance becomes invaluable:
- Server-side web scraping where you need JavaScript rendering but want to keep your scraping infrastructure separate from your application servers
- Cross-platform testing where you’re developing on Windows but testing on Linux environments
- Distributed automation where multiple test runners need to interact with centralized browser instances
- Debugging production issues where you need to inspect cookies, local storage, or network traffic on a live system
The Chrome DevTools Protocol gives us low-level access to everything Chrome can do—and I mean everything. Unlike browser automation tools that work through the DOM, CDP operates at the browser level, giving you access to cookies (including HttpOnly), network traffic, performance metrics, and much more.
The Challenge: Making Chrome Accessible Remotely
Chrome’s remote debugging feature is powerful, but getting it to work remotely involves some Linux networking quirks that aren’t immediately obvious. Let me break down the problem and solution.
The Problem
When you launch Chrome with the --remote-debugging-port flag, even if you specify --remote-debugging-address=0.0.0.0, Chrome often binds only to 127.0.0.1 (localhost). This means you can’t connect to it from another machine.
You can verify this by checking what Chrome is actually listening on:
netstat -tlnp | grep 9222
tcp 0 0 127.0.0.1:9222 0.0.0.0:* LISTEN 1891/chrome
See that 127.0.0.1? That’s the problem. It should be 0.0.0.0 to accept connections from any interface.
The Solution: socat to the Rescue
The elegant solution is to use socat (SOcket CAT) to proxy connections. We run Chrome on one port (localhost only), and use socat to forward a public-facing port to Chrome’s localhost port.
Here’s the setup on your Linux server:
# Start Chrome on localhost:9223
google-chrome \
--headless=new \
--no-sandbox \
--disable-gpu \
--remote-debugging-port=9223 \
--user-data-dir=/tmp/chrome-remote-debug &
# Use socat to proxy external 9222 to internal 9223
socat TCP-LISTEN:9222,fork,bind=0.0.0.0,reuseaddr TCP:127.0.0.1:9223 &
Now verify it’s working:
netstat -tlnp | grep 9222
tcp 0 0 0.0.0.0:9222 0.0.0.0:* LISTEN 2103/socat
netstat -tlnp | grep 9223
tcp 0 0 127.0.0.1:9223 0.0.0.0:* LISTEN 2098/chrome
Perfect! Chrome is safely listening on localhost only, while socat provides the public interface. This is actually more secure than having Chrome directly exposed.
Understanding the Chrome DevTools Protocol
Before we dive into code, let’s understand how CDP works. When Chrome runs with remote debugging enabled, it exposes two types of endpoints:
1. HTTP Endpoints (for discovery)
# Get browser version and WebSocket URL
curl http://your-server:9222/json/version
# Get list of all open pages/targets
curl http://your-server:9222/json
The /json/version endpoint returns something like:
{
"Browser": "Chrome/143.0.7499.169",
"Protocol-Version": "1.3",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36...",
"V8-Version": "14.3.127.17",
"WebKit-Version": "537.36...",
"webSocketDebuggerUrl": "ws://your-server:9222/devtools/browser/14706e92-5202-4651-aa97-a72d683bf88e"
}
2. WebSocket Endpoint (for control)
The webSocketDebuggerUrl is what we use to actually control Chrome. All CDP commands flow through this WebSocket connection using a JSON-RPC-like protocol.
Enter PuppeteerSharp
While you could manually handle WebSocket connections and craft CDP commands by hand (and I’ve done that with libraries like MasterDevs.ChromeDevTools), there’s an easier way: PuppeteerSharp.
PuppeteerSharp is a .NET port of Google’s Puppeteer library, providing a high-level API over CDP. The beauty is that it handles all the WebSocket plumbing, message routing, and protocol intricacies for you.
Here’s our complete C# application:
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using PuppeteerSharp;
namespace ChromeRemoteDebugDemo
{
class Program
{
static async Task Main(string[] args)
{
// Configuration
string remoteDebugHost = "xxxx.xxx.xxx.xxx";
int remoteDebugPort = 9222;
Console.WriteLine("=== Chrome Remote Debug - Cookie Retrieval Demo ===\n");
try
{
// Step 1: Get the WebSocket URL from Chrome
Console.WriteLine($"Connecting to http://{remoteDebugHost}:{remoteDebugPort}/json/version");
using var httpClient = new HttpClient();
string versionUrl = $"http://{remoteDebugHost}:{remoteDebugPort}/json/version";
string jsonResponse = await httpClient.GetStringAsync(versionUrl);
// Parse JSON to get webSocketDebuggerUrl
using JsonDocument doc = JsonDocument.Parse(jsonResponse);
JsonElement root = doc.RootElement;
string webSocketUrl = root.GetProperty("webSocketDebuggerUrl").GetString();
Console.WriteLine($"WebSocket URL: {webSocketUrl}\n");
// Step 2: Connect to Chrome using PuppeteerSharp
Console.WriteLine("Connecting to Chrome via WebSocket...");
var connectOptions = new ConnectOptions
{
BrowserWSEndpoint = webSocketUrl
};
var browser = await Puppeteer.ConnectAsync(connectOptions);
Console.WriteLine("Successfully connected!\n");
// Step 3: Get or create a page
var pages = await browser.PagesAsync();
IPage page;
if (pages.Length > 0)
{
page = pages[0];
Console.WriteLine($"Using existing page: {page.Url}");
}
else
{
page = await browser.NewPageAsync();
await page.GoToAsync("https://example.com");
}
// Step 4: Get ALL cookies (including HttpOnly!)
Console.WriteLine("\nRetrieving all cookies...\n");
var cookies = await page.GetCookiesAsync();
Console.WriteLine($"Found {cookies.Length} cookie(s):\n");
foreach (var cookie in cookies)
{
Console.WriteLine($"Name: {cookie.Name}");
Console.WriteLine($"Value: {cookie.Value}");
Console.WriteLine($"Domain: {cookie.Domain}");
Console.WriteLine($"Path: {cookie.Path}");
Console.WriteLine($"Secure: {cookie.Secure}");
Console.WriteLine($"HttpOnly: {cookie.HttpOnly}"); // ← This is the magic!
Console.WriteLine($"SameSite: {cookie.SameSite}");
Console.WriteLine($"Expires: {(cookie.Expires == -1 ? "Session" : DateTimeOffset.FromUnixTimeSeconds((long)cookie.Expires).ToString())}");
Console.WriteLine(new string('-', 80));
}
await browser.DisconnectAsync();
Console.WriteLine("\nDisconnected successfully.");
}
catch (Exception ex)
{
Console.WriteLine($"\n❌ ERROR: {ex.Message}");
}
}
}
}
The Key Insight: HttpOnly Cookies
Here’s what makes this approach powerful: page.GetCookiesAsync() returns ALL cookies, including HttpOnly ones.
In a normal web page, JavaScript cannot access HttpOnly cookies—that’s the whole point of the HttpOnly flag. It’s a security feature that prevents XSS attacks from stealing session tokens. But when you’re operating at the CDP level, you’re not bound by JavaScript’s restrictions. You’re talking directly to Chrome’s internals.
This is incredibly useful for:
- Session management in automation: You can extract session cookies from one browser session and inject them into another
- Security testing: Verify that sensitive cookies are properly marked HttpOnly
- Debugging authentication issues: See exactly what cookies are being set by your backend
- Web scraping: Maintain authenticated sessions across multiple scraper instances
Setting Up the Project
Create a new console application:
dotnet new console -n ChromeRemoteDebugDemo
cd ChromeRemoteDebugDemo
Add PuppeteerSharp:
dotnet add package PuppeteerSharp
Your .csproj should look like:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="PuppeteerSharp" Version="20.2.4" />
</ItemGroup>
</Project>
Running the Demo
On your Linux server:
# Install socat if needed
apt-get install socat -y
# Start Chrome on internal port 9223
google-chrome \
--headless=new \
--no-sandbox \
--disable-gpu \
--remote-debugging-port=9223 \
--user-data-dir=/tmp/chrome-remote-debug &
# Proxy external 9222 to internal 9223
socat TCP-LISTEN:9222,fork,bind=0.0.0.0,reuseaddr TCP:127.0.0.1:9223 &
On your Windows development machine:
dotnet run
You should see output like:
=== Chrome Remote Debug - Cookie Retrieval Demo ===
Connecting to http://xxxx.xxx.xxx.xxx:9222/json/version
WebSocket URL: ws://xxxx.xxx.xxx.xxx:9222/devtools/browser/14706e92-5202-4651-aa97-a72d683bf88e
Connecting to Chrome via WebSocket...
Successfully connected!
Using existing page: https://example.com
Retrieving all cookies...
Found 2 cookie(s):
Name: _ga
Value: GA1.2.123456789.1234567890
Domain: .example.com
Path: /
Secure: True
HttpOnly: False
SameSite: Lax
Expires: 2026-12-27 10:30:45
--------------------------------------------------------------------------------
Name: session_id
Value: abc123xyz456
Domain: example.com
Path: /
Secure: True
HttpOnly: True ← Notice this!
SameSite: Strict
Expires: Session
--------------------------------------------------------------------------------
Disconnected successfully.
Security Considerations
Before you deploy this in production, consider these security implications:
1. Firewall Configuration
Only expose port 9222 to trusted networks. If you’re running this on a cloud server:
# Allow only your specific IP
sudo ufw allow from YOUR.IP.ADDRESS to any port 9222
Or better yet, use an SSH tunnel and don’t expose the port at all:
# On Windows, create a tunnel
ssh -N -L 9222:localhost:9222 user@remote-server
# Then connect to localhost:9222 in your code
2. Authentication
The Chrome DevTools Protocol has no built-in authentication. Anyone who can connect to the debugging port has complete control over Chrome. This includes:
- Reading all page content
- Executing arbitrary JavaScript
- Accessing all cookies (as we’ve demonstrated)
- Intercepting and modifying network requests
In production, you should:
- Use SSH tunnels instead of exposing the port
- Run Chrome in a sandboxed environment
- Use short-lived debugging sessions
- Monitor for unauthorized connections
3. Resource Limits
A runaway Chrome instance can consume significant resources. Consider:
# Limit Chrome's memory usage
google-chrome --headless=new \
--max-old-space-size=512 \
--remote-debugging-port=9223 \
--user-data-dir=/tmp/chrome-remote-debug
Beyond Cookies: What Else Can You Do?
The Chrome DevTools Protocol is incredibly powerful. Here are some other things you can do with this same setup:
Take Screenshots
await page.ScreenshotAsync("/path/to/screenshot.png");
Monitor Network Traffic
await page.SetRequestInterceptionAsync(true);
page.Request += (sender, e) =>
{
Console.WriteLine($"Request: {e.Request.Url}");
e.Request.ContinueAsync();
};
Execute JavaScript
var title = await page.EvaluateExpressionAsync<string>("document.title");
Modify Cookies
await page.SetCookieAsync(new CookieParam
{
Name = "test",
Value = "123",
Domain = "example.com",
HttpOnly = true, // Can set HttpOnly from CDP!
Secure = true
});
Emulate Mobile Devices
await page.EmulateAsync(new DeviceDescriptorOptions
{
Viewport = new ViewPortOptions { Width = 375, Height = 667 },
UserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)"
});
Comparing Approaches
You might be wondering how this compares to other approaches:
PuppeteerSharp vs. Selenium
Selenium uses the WebDriver protocol, which is a W3C standard but higher-level and more abstracted. PuppeteerSharp/CDP gives you lower-level access to Chrome specifically.
- Selenium: Better for cross-browser testing, more stable API
- PuppeteerSharp: More powerful Chrome-specific features, faster, lighter weight
PuppeteerSharp vs. Raw CDP Libraries
You could use libraries like MasterDevs.ChromeDevTools or ChromeProtocol for more direct CDP access:
// With MasterDevs.ChromeDevTools
var session = new ChromeSession(webSocketUrl);
var cookies = await session.SendAsync(new GetCookiesCommand());
Low-level CDP libraries:
- Pros: More control, can use experimental CDP features
- Cons: More verbose, have to handle protocol details
PuppeteerSharp:
- Pros: High-level API, actively maintained, comprehensive documentation
- Cons: Abstracts away some CDP features
For most use cases, PuppeteerSharp hits the sweet spot between power and ease of use.
Troubleshooting Common Issues
“Could not connect to Chrome debugging endpoint”
Check firewall:
sudo ufw status
sudo iptables -L -n | grep 9222
Verify Chrome is running:
ps aux | grep chrome
netstat -tlnp | grep 9222
Test locally first:
curl http://localhost:9222/json/version
“No cookies found”
This is normal if the page hasn’t set any cookies. Navigate to a site that does:
await page.GoToAsync("https://github.com");
var cookies = await page.GetCookiesAsync();
Chrome crashes or hangs
Add more stability flags:
google-chrome \
--headless=new \
--no-sandbox \
--disable-gpu \
--disable-dev-shm-usage \
--disable-setuid-sandbox \
--remote-debugging-port=9223 \
--user-data-dir=/tmp/chrome-remote-debug
Real-World Use Case: Session Management
Here’s a practical example of how I’ve used this in production—managing authenticated sessions for web scraping:
public class SessionManager
{
private readonly string _remoteChrome;
public async Task<CookieParam[]> LoginAndGetSession(string username, string password)
{
var browser = await ConnectToRemoteChrome();
var page = await browser.NewPageAsync();
// Perform login
await page.GoToAsync("https://example.com/login");
await page.TypeAsync("#username", username);
await page.TypeAsync("#password", password);
await page.ClickAsync("#login-button");
await page.WaitForNavigationAsync();
// Extract all cookies (including HttpOnly session tokens!)
var cookies = await page.GetCookiesAsync();
await browser.DisconnectAsync();
// Store these cookies for later use
return cookies;
}
public async Task ReuseSession(CookieParam[] cookies)
{
var browser = await ConnectToRemoteChrome();
var page = await browser.NewPageAsync();
// Inject the saved cookies
await page.SetCookieAsync(cookies);
// Now you're authenticated!
await page.GoToAsync("https://example.com/dashboard");
// Do your work...
}
}
This allows you to:
- Log in once in a “master” browser
- Extract the session cookies (including HttpOnly auth tokens)
- Distribute those cookies to multiple scraper instances
- All scrapers are now authenticated without re-logging in
Conclusion
The Chrome DevTools Protocol opens up a world of possibilities for browser automation and debugging. By combining it with PuppeteerSharp and a bit of Linux networking knowledge, you can:
- Control Chrome instances running anywhere on your network
- Access all browser data, including HttpOnly cookies
- Build powerful automation and testing tools
- Debug production issues remotely
The key takeaways:
- Use socat to proxy Chrome’s localhost debugging port to external interfaces
- PuppeteerSharp provides the easiest way to interact with CDP from C#
- CDP gives you superpowers that normal JavaScript can’t access
- Security matters—only expose debugging ports to trusted networks
The complete code from this post is available on GitHub (replace with your actual link). If you found this useful, consider giving it a star!
Have you used the Chrome DevTools Protocol in your projects? What creative uses have you found for it? Drop a comment below—I’d love to hear your experiences!
Further Reading
- Chrome DevTools Protocol Documentation
- PuppeteerSharp Documentation
- Puppeteer (Node.js) Guide
- socat Man Page
Tags: C#, Chrome, DevTools Protocol, PuppeteerSharp, Web Automation, Browser Automation, Linux, socat
How to Check All AWS Regions for Deprecated Python 3.9 Lambda Functions (PowerShell Guide)

If you’ve received an email from AWS notifying you that Python 3.9 is being deprecated for AWS Lambda, you’re not alone. As runtimes reach End-Of-Life, AWS sends warnings so you can update your Lambda functions before support officially ends.
The key question is:
How do you quickly check every AWS region to see where you’re still using Python 3.9?
AWS only gives you a single-region example in their email, but many teams have functions deployed globally. Fortunately, you can automate a full multi-region check using a simple PowerShell script.
This post shows you exactly how to do that.
🚨 Why You Received the Email
AWS is ending support for Python 3.9 in AWS Lambda.
After the deprecation dates:
- No more security patches
- No AWS technical support
- You won’t be able to create/update functions using Python 3.9
- Your functions will still run, but on an unsupported runtime
To avoid risk, you should upgrade these functions to Python 3.10, 3.11, or 3.12.
But first, you need to find all the functions using Python 3.9 — across all regions.
✔️ Prerequisites
Make sure you have:
- AWS CLI installed
- AWS credentials configured (via
aws configure) - Permissions to run:
lambda:ListFunctionsec2:DescribeRegions
🧪 Step 1 — Verify AWS CLI Access
Run this to confirm your CLI is working:
aws sts get-caller-identity --region eu-west-1
If it returns your AWS ARN, you’re good to go.
If you see “You must specify a region”, set a default region:
aws configure set region eu-west-1
📝 Step 2 — PowerShell Script to Check Python 3.9 in All Regions
Save this as aws-lambda-python39-check.ps1 (or any name you prefer):
# Get all AWS regions (forcing region so the call always works)
$regions = (aws ec2 describe-regions --region us-east-1 --query "Regions[].RegionName" --output text) -split "\s+"
foreach ($region in $regions) {
Write-Host "Checking region: $region ..."
$functions = aws lambda list-functions `
--region $region `
--query "Functions[?Runtime=='python3.9'].FunctionArn" `
--output text
if ($functions) {
Write-Host " → Found Python 3.9 functions:"
Write-Host " $functions"
} else {
Write-Host " → No Python 3.9 functions found."
}
}
This script does three things:
- Retrieves all AWS regions
- Loops through each region
- Prints any Lambda functions that still use Python 3.9
It handles the common AWS CLI error:
You must specify a region
by explicitly using --region us-east-1 when retrieving the region list.
▶️ Step 3 — Run the Script
Open PowerShell in the folder where your script is saved:
.\aws-lambda-python39-check.ps1
You’ll see output like:
Checking region: eu-west-1 ...
→ Found Python 3.9 functions:
arn:aws:lambda:eu-west-1:123456789012:function:my-old-function
Checking region: us-east-1 ...
→ No Python 3.9 functions found.
If no functions appear, you’re fully compliant.
🛠️ What to Do Next
For each function identified, update the runtime:
aws lambda update-function-configuration `
--function-name MyFunction `
--runtime python3.12
If you package dependencies manually (ZIP deployments), ensure you rebuild them using the new Python version.
🎉 Summary
AWS’s deprecation emails can be slightly alarming, but the fix is simple:
- Scan all regions
- Identify Python 3.9 Lambda functions
- Upgrade them in advance of the cutoff date
With the PowerShell script above, you can audit your entire AWS account in seconds.
How to Integrate the RegCheck Vehicle Lookup #API with #OpenAI Actions
In today’s AI-driven world, connecting specialized APIs to large language models opens up powerful possibilities. One particularly useful integration is connecting vehicle registration lookup services to OpenAI’s custom GPTs through Actions. In this tutorial, we’ll walk through how to integrate the RegCheck API with OpenAI Actions, enabling your custom GPT to look up vehicle information from over 30 countries.
What is RegCheck?
RegCheck is a comprehensive vehicle data API that provides detailed information about vehicles based on their registration numbers (license plates). With support for countries including the UK, USA, Australia, and most of Europe, it’s an invaluable tool for automotive businesses, insurance companies, and vehicle marketplace platforms.
Why Integrate with OpenAI Actions?
OpenAI Actions allow custom GPTs to interact with external APIs, extending their capabilities beyond text generation. By integrating RegCheck, you can create a GPT assistant that:
- Instantly looks up vehicle specifications for customers
- Provides insurance quotes based on real vehicle data
- Assists with vehicle valuations and sales listings
- Answers detailed questions about specific vehicles
Prerequisites
Before you begin, you’ll need:
- An OpenAI Plus subscription (for creating custom GPTs)
- A RegCheck API account with credentials
- Basic familiarity with OpenAPI specifications
Step-by-Step Integration Guide
Step 1: Create Your Custom GPT
Navigate to OpenAI’s platform and create a new custom GPT. Give it a name like “Vehicle Lookup Assistant” and configure its instructions to handle vehicle-related queries.
Step 2: Add the OpenAPI Schema
In your GPT configuration, navigate to the “Actions” section and add the following OpenAPI specification:
yaml
openapi: 3.0.0
info:
title: RegCheck Vehicle Lookup API
version: 1.0.0
description: API for looking up vehicle registration information across multiple countries
servers:
- url: https://www.regcheck.org.uk/api/json.aspx
paths:
/Check/{registration}:
get:
operationId: checkUKVehicle
summary: Get details for a vehicle in the UK
parameters:
- name: registration
in: path
required: true
schema:
type: string
description: UK vehicle registration number
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
/CheckSpain/{registration}:
get:
operationId: checkSpainVehicle
summary: Get details for a vehicle in Spain
parameters:
- name: registration
in: path
required: true
schema:
type: string
description: Spanish vehicle registration number
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
/CheckFrance/{registration}:
get:
operationId: checkFranceVehicle
summary: Get details for a vehicle in France
parameters:
- name: registration
in: path
required: true
schema:
type: string
description: French vehicle registration number
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
/VinCheck/{vin}:
get:
operationId: checkVehicleByVin
summary: Get details for a vehicle by VIN number
parameters:
- name: vin
in: path
required: true
schema:
type: string
description: Vehicle Identification Number
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
Note: You can expand this schema to include additional endpoints for other countries as needed. The RegCheck API supports over 30 countries.
Step 3: Configure Authentication
- In the Authentication section, select Basic authentication
- Enter your RegCheck API username
- Enter your RegCheck API password
- OpenAI will securely encrypt and store these credentials
The authentication header will be automatically included in all API requests made by your GPT.
Step 4: Test Your Integration
Use the built-in test feature in the Actions panel to verify the connection:
- Select the
checkUKVehicleoperation - Enter a test registration like
YYO7XHH - Click “Test” to see the response
You should receive a JSON response with vehicle details including make, model, year, engine size, and more.
Step 5: Configure GPT Instructions
Update your GPT’s instructions to effectively use the new Actions:
You are a vehicle information assistant. When users provide a vehicle
registration number, use the appropriate CheckVehicle action based on
the country. Present the information in a clear, user-friendly format.
Always ask which country the registration is from if not specified.
Provide helpful context about the vehicle data returned.
Example Use Cases
Once integrated, your GPT can handle queries like:
User: “What can you tell me about UK registration YYO7XHH?”
GPT: [Calls checkUKVehicle action] “This is a 2007 Peugeot 307 X-line with a 1.4L petrol engine. It’s a 5-door manual transmission vehicle with right-hand drive…”
User: “Look up Spanish plate 0075LTJ”
GPT: [Calls checkSpainVehicle action] “Here’s the information for that Spanish vehicle…”
Best Practices and Considerations
API Limitations
- The RegCheck API is currently in BETA and may change without notice
- Consider implementing error handling in your GPT instructions
- Be aware of rate limits on your API account
Privacy and Security
- Never expose API credentials in your GPT’s instructions or responses
- Inform users that vehicle lookups are being performed
- Comply with data protection regulations in your jurisdiction
Optimizing Performance
- Cache frequently requested vehicle information where appropriate
- Use the most specific endpoint (e.g., CheckSpain vs. generic Check)
- Consider implementing fallback behavior for failed API calls
Expanding the Integration
The RegCheck API offers many more endpoints you can integrate:
- UKMOT: Access MOT test history for UK vehicles
- WheelSize: Get wheel and tire specifications
- CarSpecifications: Retrieve detailed specs by make/model/year
- Country-specific checks: Add support for Australia, USA, and 25+ other countries
Simply add these endpoints to your OpenAPI schema following the same pattern.
Troubleshooting Common Issues
Authentication Errors: Double-check your username and password are correct in the Authentication settings.
404 Not Found: Verify the registration format matches the country’s standard format.
Empty Responses: Some vehicles may not have complete data in the RegCheck database.
Conclusion
Integrating the RegCheck API with OpenAI Actions transforms a standard GPT into a powerful vehicle information assistant. Whether you’re building tools for automotive dealerships, insurance platforms, or customer service applications, this integration provides instant access to comprehensive vehicle data from around the world.
The combination of AI’s natural language understanding with RegCheck’s extensive vehicle database creates a seamless user experience that would have required significant custom development just a few years ago.
Ready to get started? Create your RegCheck account, set up your custom GPT, and start building your vehicle lookup assistant today!