How to Solve reCAPTCHA in Deno: Permissions and Timeouts

To solve reCAPTCHA in Deno, import the CapSkip Node.js SDK through an npm specifier and read the solver’s address from the environment yourself. Then run the script with permission flags that name only CapSkip and your site. CapSkip runs on your own Windows PC, so there is no per-solve bill. The code is short. Deno’s permission model is the hard part. The SDK reports a blocked call as a network error. A CapSkip that goes silent hangs the script with no error and no exit. And the usual way to add a timeout keeps the process alive for the whole budget. This guide covers each of those.
What you need
- CapSkip running on a Windows PC, with the reCAPTCHA solver switched on. The setup guide walks through the first start.
- Deno 2. Everything below was run on Windows 11 with Deno 2.9.6, and again on 2.9.7, with capskip 1.3.0 from npm.
- A PowerShell prompt. The commands use PowerShell syntax.
- A page with a reCAPTCHA v2 checkbox in a form. The example logs in to a Django-style form that carries a CSRF token. Change the field names to match your site.
- An address for the solver. In Local mode CapSkip answers on 127.0.0.1 for that device only. In Server mode it listens on your network address or public IP, so a script on another machine can call it over the API. Both are set under connection settings.
The measurements below come from a mock CapSkip and a mock login page on the same PC, used with the real Deno and the real SDK. The permission errors, the polling schedule and the hang behaviour belong to Deno and the SDK, so they hold for your own site too. Solve times and the accept and reject rules of the login page are the mock’s own.
Step 1: install Deno and create the project
Install Deno with its official installer, or with winget. Deno’s installation page lists the other options, such as Scoop and Chocolatey.
# PowerShell: the official installer (or: winget install DenoLand.Deno) irm https://deno.land/install.ps1 | iex deno --version
Then make a project folder and put a deno.json in it before you run anything else. The import map pins the SDK version and lets the script say "capskip" instead of a long npm specifier.
{
"imports": {
"capskip": "npm:[email protected]"
}
}Do not run deno add before that file exists. If a package.json sits in a folder above your project, which is common in a user profile, Deno treats the project as a Node project. An import of npm:[email protected] then fails with Could not find "capskip" in a node_modules folder. An import with no version quietly loads whichever capskip is installed up there. On the test PC that was an older 1.0.2. Running deno add with no deno.json even rewrote that parent package.json. A deno.json in your own folder anchors everything to your project.
Step 2: import the SDK and read the settings
Save the code from Steps 2 to 5 in one file called solve.js, in the order shown. The whole file is repeated near the end of this guide. Deno runs it as an ES module, so the SDK README’s require call fails here with require is not defined. Use the named import. A default import fails too, because it hands you the whole exports object: new CapSkip() throws CapSkip is not a constructor.
import { CapSkip } from "capskip";
// The SDK never reads the environment, so read it here and pass it in.
const host = Deno.env.get("CAPSKIP_HOST") ?? "127.0.0.1";
const port = Number(Deno.env.get("CAPSKIP_PORT") ?? 8080);
const apiKey = Deno.env.get("CAPSKIP_API_KEY") ?? "capskip";
const solver = new CapSkip({ host, port, apiKey });
// The login page to solve, passed on the command line.
const pageUrl = Deno.args[0] ?? "https://example.com/login";The SDK does not read environment variables on its own, so the script reads them and passes them to the constructor. The defaults make the file run with no setup. CAPSKIP_API_KEY only matters if you switched on API key validation in CapSkip.
Step 3: fetch the page and keep its cookies
Deno’s fetch has no cookie jar, so the script keeps one. It reads every Set-Cookie line with getSetCookie. Calling headers.get on set-cookie instead glues the lines into one string. A cookie’s Expires date contains a comma, so a naive split on commas cuts the cookies in the wrong places.
// Deno's fetch keeps no cookies, so collect every Set-Cookie line.
// (headers.get("set-cookie") would glue them into one string.)
const jar = new Map();
/** @param {Response} response */
function keepCookies(response) {
for (const line of response.headers.getSetCookie()) {
const [pair] = line.split(";");
const eq = pair.indexOf("=");
jar.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
}
}
const cookies = () => Array.from(jar, ([k, v]) => `${k}=${v}`).join("; ");Next the script loads the login page and reads the sitekey and the CSRF token out of it. Adjust the two patterns to your page’s markup.
const page = await fetch(pageUrl);
keepCookies(page);
const html = await page.text();
// Adjust these two patterns to your page's markup.
const sitekey = html.match(/data-sitekey="([^"]+)"/)?.[1];
const csrf = html.match(/name="csrfmiddlewaretoken" value="([^"]+)"/)?.[1];
if (!sitekey || !csrf) throw new Error("sitekey or CSRF token not found");Step 4: ask CapSkip for a token
One call does the work. It submits the task, polls for the answer and returns the token. The SDK polls at once, then waits a quarter of a second and doubles the wait each time, up to 5 seconds. A solve that CapSkip finished in one second came back in about 1.8 seconds in testing, the same as under Node.
// code is typed "string or object" (Capy shares the field), so
// String() keeps deno check quiet.
const { code } = await solver.recaptcha(sitekey, pageUrl);
const token = String(code);The result has a code field, and that is the token. The SDK’s type file declares it as a string or an object, because some CAPTCHA types return an object in the same field. String() turns it into a plain string. A later section adds a timeout to this call.
Step 5: post the form and check the result
The script posts the token in the g-recaptcha-response field next to the other form fields. Two details matter. Deno’s fetch sends no Referer, and a strict site such as Django over HTTPS rejects a form post without one, so the script sets it. And the fetch call uses redirect manual, so the 302 comes back and its new session cookie can be read. With the default of following redirects, the cookie on the 302 is lost and the login lands back on the form with a 200.
// fetch sends no Referer, and a strict site rejects the post without
// one. redirect "manual" keeps the 302 so we can read its new cookie.
const response = await fetch(pageUrl, {
method: "POST",
redirect: "manual",
headers: { Cookie: cookies(), Referer: pageUrl },
body: new URLSearchParams({
csrfmiddlewaretoken: csrf,
username: "YOUR_USERNAME",
password: "YOUR_PASSWORD",
"g-recaptcha-response": token,
}),
});
keepCookies(response);A rejected login often answers 200 with the form again. So the script treats anything but a 302 as a failure, then looks for a marker that only a logged-in page shows. Swap Welcome back for text from your own site. A script that reports success for a rejected login is worse than one that fails.
// A good login answers 302 and rotates the session cookie.
if (response.status !== 302) {
const body = await response.text();
throw new Error(`login rejected: ${response.status} ${body}`);
}
const next = new URL(response.headers.get("location") ?? "/", pageUrl);
const home = await fetch(next, { headers: { Cookie: cookies() } });
// Swap "Welcome back" for a marker your own site shows after login.
const ok = (await home.text()).includes("Welcome back");
console.log(ok ? "logged in" : "NOT logged in");
if (!ok) Deno.exit(2);Step 6: run it with the smallest permission set
Deno denies network, environment and file access until you allow it. To solve reCAPTCHA in Deno, this script needs two things, and the command below grants exactly those.
deno run ` --allow-env=CAPSKIP_HOST,CAPSKIP_PORT,CAPSKIP_API_KEY ` --allow-net=127.0.0.1:8080,example.com ` solve.js https://example.com/login
The env list names the three CAPSKIP variables. The net list names CapSkip’s address and port, and the site. Replace example.com and the page address with your own site. Take any one entry out and the run fails, and the error names what is missing.
| Permission | What it allows | What you see without it |
|---|---|---|
| env, three names | Reading the CAPSKIP variables | NotCapable: Requires env access to "CAPSKIP_HOST", followed by advice to run again with the env flag. A variable that is not set is denied too, so the defaults do not help. |
| net, CapSkip’s host and port | The SDK’s calls to CapSkip | A NetworkException whose message is Requires net access to "127.0.0.1:8080", followed by the same advice for the net flag. |
| net, the site | Your fetch calls to the page and the form | A NotCapable error that names the site. |
A few rules about the net list. A host with no port allows every port, so example.com covers both 80 and 443. An entry with a port allows only that port. 127.0.0.1 and 127.0.0.2 are different hosts. And a redirect to another host name needs that host on the list too. A page that bounces from example.com to www.example.com fails on the second hop until you add it. Nothing else was needed: no read, run, sys or import permission.
In a terminal, Deno asks instead of failing. It prompts once for each variable and each host, and answering no gives the same errors. In a scheduled job there is no terminal, so it fails straight away. The allow-all flag (-A) removes every one of these checks, so keep it for a quick experiment.
You can put the same command in deno.json as a task, so you type less. The first block below is deno.json with the import map and a solve task, and the second sets the environment in PowerShell and runs it. Deno appends the page address you pass after the task name.
{
"imports": {
"capskip": "npm:[email protected]"
},
"tasks": {
"solve": "deno run --allow-env=CAPSKIP_HOST,CAPSKIP_PORT,CAPSKIP_API_KEY --allow-net=127.0.0.1:8080,example.com solve.js"
}
}$env:CAPSKIP_HOST = "127.0.0.1" $env:CAPSKIP_PORT = "8080" deno task solve https://example.com/login
Use the IP address for CAPSKIP_HOST. If you set a name such as localhost, that name must itself be in the net list, or the SDK fails with getaddrinfo EPERM localhost. Listing it is a trap too. With CapSkip not running, localhost makes Deno try the IPv6 address [::1], which is not on the list. The error is thrown from a timer, not from the SDK call, so it is not a NetworkException and try and catch cannot catch it.
Tell a blocked call from a CapSkip that is not running
Both arrive as a NetworkException, so a handler that always says "is CapSkip running?" sends you the wrong way. The difference is in the message and the cause. A blocked call has a message that starts with Requires net access, and its cause is Deno’s NotCapable error. If CAPSKIP_HOST is a name, the message is getaddrinfo EPERM and the cause has the code EPERM instead. A CapSkip that is not running has a message that starts with connect ECONNREFUSED. It also takes about two seconds to fail under Deno on Windows, against a few milliseconds under Node. A script that seems to pause before reporting it is not hung. The function below checks for both kinds of block. The full script at the end calls it in a catch block around the whole flow.
import { ApiException, NetworkException, TimeoutException } from "capskip";
// Deno's permission errors arrive wrapped in NetworkException. The
// cause is NotCapable for an IP host, and has the code EPERM for a
// host name. Tell them apart from a CapSkip that is not running.
/** @param {any} err */
function explain(err) {
const cause = err?.cause;
if (err instanceof NetworkException) {
const denied = cause instanceof Deno.errors.NotCapable ||
cause?.code === "EPERM";
return denied
? `Deno blocked the call, add a flag: ${err.message}`
: `CapSkip is not reachable: ${err.message || cause?.code}`;
}
if (err instanceof ApiException) return `CapSkip replied: ${err.message}`;
if (err instanceof TimeoutException) return `Too slow: ${err.message}`;
return String(err?.stack ?? err);
}Stop a silent CapSkip from hanging the script
The SDK sets no timeout on its connection. If CapSkip accepts a call and then never answers, the script waits forever. Deno prints nothing and does not exit. The SDK’s recaptchaTimeout setting does not help. It is only checked between polls, so it cannot fire while the submit call or a poll is waiting for an answer. Node behaves the same way. Without any wrapper the only way out is Ctrl+C.
Bound the call yourself. The usual tool is AbortSignal.timeout, and in Deno it is the wrong one here. A timeout signal with an abort listener keeps the process alive until the timer fires. A 30 second budget around a solve that took 1.8 seconds made the script exit after 30 seconds. A plain setTimeout that you clear when the call settles exits at once on success.
// The SDK sets no socket timeout, so a CapSkip that accepts the call
// and never answers would hang the script. Race it against a timer,
// and clear the timer so the process can exit on success.
/**
* @param {Promise<any>} promise
* @param {number} ms
*/
function withTimeout(promise, ms) {
const { promise: limit, reject } = Promise.withResolvers();
const timer = setTimeout(
() => reject(new Error(`no answer in ${ms} ms`)),
ms,
);
return Promise.race([promise, limit]).finally(() => clearTimeout(timer));
}Then replace the Step 4 block with this one. It wraps the same solve call. Paste explain and withTimeout anywhere above the lines that use them. The SDK gives up after 300 seconds by itself, so a budget of 330 seconds only fires when CapSkip goes silent.
// The SDK gives up after 300 s by itself, so a 330 s budget only
// fires when CapSkip goes silent. code is typed "string or object"
// (Capy shares the field), so String() hands the form a plain string.
const solving = solver.recaptcha(sitekey, pageUrl);
const { code } = await withTimeout(solving, 330_000);
const token = String(code);When the timer wins, the pending connection is still open, so the catch block in the full example ends with Deno.exit. Without it the process would keep waiting on the socket. In testing, a CapSkip that never answered the submit and one that never answered the result polls both ended with the error after the budget. The exit code was 1.
Type-check the script
Deno can check JavaScript against the SDK’s bundled types, but it does nothing for a plain .js file unless you ask. A deno check on a plain .js file reports nothing, even when the code has a type error. Switching checkJs on in deno.json makes the check real.
{
"imports": {
"capskip": "npm:[email protected]"
},
"compilerOptions": {
"checkJs": true
}
}deno check solve.js
With the check on, a misspelled constructor option such as hots for host is an error. A misspelled option inside the solve call is not, because the options type accepts any key, so a typo there compiles and does nothing. Treat the check as a help, not a guarantee.
Compile it to a single exe
Because the permissions are part of the command, deno compile can bake them in. A script that solves reCAPTCHA in Deno then ships as one solve.exe that runs on a PC with no Deno installed. That suits a Windows machine that runs CapSkip and a script side by side.
deno compile ` --allow-env=CAPSKIP_HOST,CAPSKIP_PORT,CAPSKIP_API_KEY ` --allow-net=127.0.0.1:8080,example.com ` --output solve.exe solve.js .\solve.exe https://example.com/login
Four things to know. The first compile downloads a runtime of about 33 MB from dl.deno.land, and the exe is about 80 MB. Environment variables still work, so CAPSKIP_PORT is honoured. The permissions are fixed at compile time, and the exe has no flag to widen them: pass the allow-all flag and it becomes the first program argument. And in the exe, a denied call says to specify the permission during compilation.
A launch that looks like Task Scheduler’s ran the exe and exited 0 in testing. It used System32 as the working folder, no console and a PATH without Deno. A real scheduled task was not registered, so read the guide to solving CAPTCHAs in Windows Task Scheduler jobs for the time limit and restart settings that matter there.
Running CapSkip on another machine
The script and the solver do not have to share a PC. Switch CapSkip to Server mode under connection settings, and it listens on your network address or public IP. Then change two things: CAPSKIP_HOST and the net list. If you forget the new address in the net list, the error names it.
$env:CAPSKIP_HOST = "192.168.1.50" $env:CAPSKIP_PORT = "8080" $env:CAPSKIP_API_KEY = "YOUR_API_KEY" deno run ` --allow-env=CAPSKIP_HOST,CAPSKIP_PORT,CAPSKIP_API_KEY ` --allow-net=192.168.1.50:8080,example.com ` solve.js https://example.com/login
Use a static public IP if the route crosses the internet. Turn on API key validation, with the same key in CAPSKIP_API_KEY as the block above sets. CapSkip opens its port to every address when Server mode starts. To limit access to the machines you expect, add a Windows Firewall block rule for the others. The solver is still your own Windows machine, and solves are still unmetered.
The full script
Here is solve.js in one piece, with the timeout wrapper and the error handler in place. Save it next to deno.json and run it with the command from Step 6.
import {
ApiException,
CapSkip,
NetworkException,
TimeoutException,
} from "capskip";
// The SDK never reads the environment, so read it here and pass it in.
const host = Deno.env.get("CAPSKIP_HOST") ?? "127.0.0.1";
const port = Number(Deno.env.get("CAPSKIP_PORT") ?? 8080);
const apiKey = Deno.env.get("CAPSKIP_API_KEY") ?? "capskip";
const solver = new CapSkip({ host, port, apiKey });
// The login page to solve, passed on the command line.
const pageUrl = Deno.args[0] ?? "https://example.com/login";
// Deno's fetch keeps no cookies, so collect every Set-Cookie line.
// (headers.get("set-cookie") would glue them into one string.)
const jar = new Map();
/** @param {Response} response */
function keepCookies(response) {
for (const line of response.headers.getSetCookie()) {
const [pair] = line.split(";");
const eq = pair.indexOf("=");
jar.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
}
}
const cookies = () => Array.from(jar, ([k, v]) => `${k}=${v}`).join("; ");
// Deno's permission errors arrive wrapped in NetworkException. The
// cause is NotCapable for an IP host, and has the code EPERM for a
// host name. Tell them apart from a CapSkip that is not running.
/** @param {any} err */
function explain(err) {
const cause = err?.cause;
if (err instanceof NetworkException) {
const denied = cause instanceof Deno.errors.NotCapable ||
cause?.code === "EPERM";
return denied
? `Deno blocked the call, add a flag: ${err.message}`
: `CapSkip is not reachable: ${err.message || cause?.code}`;
}
if (err instanceof ApiException) return `CapSkip replied: ${err.message}`;
if (err instanceof TimeoutException) return `Too slow: ${err.message}`;
return String(err?.stack ?? err);
}
// The SDK sets no socket timeout, so a CapSkip that accepts the call
// and never answers would hang the script. Race it against a timer,
// and clear the timer so the process can exit on success.
/**
* @param {Promise<any>} promise
* @param {number} ms
*/
function withTimeout(promise, ms) {
const { promise: limit, reject } = Promise.withResolvers();
const timer = setTimeout(
() => reject(new Error(`no answer in ${ms} ms`)),
ms,
);
return Promise.race([promise, limit]).finally(() => clearTimeout(timer));
}
try {
const page = await fetch(pageUrl);
keepCookies(page);
const html = await page.text();
// Adjust these two patterns to your page's markup.
const sitekey = html.match(/data-sitekey="([^"]+)"/)?.[1];
const csrf = html.match(/name="csrfmiddlewaretoken" value="([^"]+)"/)?.[1];
if (!sitekey || !csrf) throw new Error("sitekey or CSRF token not found");
// The SDK gives up after 300 s by itself, so a 330 s budget only
// fires when CapSkip goes silent. code is typed "string or object"
// (Capy shares the field), so String() hands the form a plain string.
const solving = solver.recaptcha(sitekey, pageUrl);
const { code } = await withTimeout(solving, 330_000);
const token = String(code);
// fetch sends no Referer, and a strict site rejects the post without
// one. redirect "manual" keeps the 302 so we can read its new cookie.
const response = await fetch(pageUrl, {
method: "POST",
redirect: "manual",
headers: { Cookie: cookies(), Referer: pageUrl },
body: new URLSearchParams({
csrfmiddlewaretoken: csrf,
username: "YOUR_USERNAME",
password: "YOUR_PASSWORD",
"g-recaptcha-response": token,
}),
});
keepCookies(response);
// A good login answers 302 and rotates the session cookie.
if (response.status !== 302) {
const body = await response.text();
throw new Error(`login rejected: ${response.status} ${body}`);
}
const next = new URL(response.headers.get("location") ?? "/", pageUrl);
const home = await fetch(next, { headers: { Cookie: cookies() } });
// Swap "Welcome back" for a marker your own site shows after login.
const ok = (await home.text()).includes("Welcome back");
console.log(ok ? "logged in" : "NOT logged in");
if (!ok) Deno.exit(2);
} catch (err) {
console.error(explain(err));
Deno.exit(1);
}Common errors when you solve reCAPTCHA in Deno
Without a try block, Deno prints the class name first, as in the first column. The full script prints the same messages after its own prefix.
| What you see | Cause | Fix |
|---|---|---|
| Requires env access to "CAPSKIP_HOST" | The env permission is missing a name | List all three CAPSKIP names |
| NetworkException: Requires net access to "127.0.0.1:8080" | CapSkip’s host and port are not in the net list | Add them, and add the site too |
| NetworkException: connect ECONNREFUSED, after about 2 seconds | CapSkip is not running, or the port is wrong | Start CapSkip and check the port under connection settings |
| NetworkException: getaddrinfo EPERM localhost | A host name is in use but only the IP is in the net list | Use 127.0.0.1 in CAPSKIP_HOST |
| ReferenceError: require is not defined | A CommonJS call in an ES module | Use the named import |
| TypeError: CapSkip is not a constructor | A default import gives the whole exports object | Import { CapSkip } by name |
| Could not find "capskip" in a node_modules folder | A package.json above the project takes over | Put a deno.json in the project folder |
| No output, no exit | CapSkip accepted the call and never answered | Wrap the call in withTimeout and call Deno.exit |
| ApiException: ERROR_CAPTCHA_UNSOLVABLE | CapSkip tried and failed | Submit a fresh task, see the notes on ERROR_CAPTCHA_UNSOLVABLE |
FAQ
Can I skip the flags and use allow-all?
It works, and it removes the point of running under Deno. The script then has network, file and environment access to everything, including the packages it imports. One list of three names and one list of two hosts are short enough to keep.
Does this work on Deno Deploy or another hosted runtime?
Deno Deploy and other hosted runtimes were not tested here. A server in someone else’s data centre cannot reach 127.0.0.1 on your PC. So run CapSkip in Server mode on a Windows machine you control, with a static public IP, and point CAPSKIP_HOST at it. The platform must also allow outbound connections to that address and port, so check its network rules. The guide to solving CAPTCHAs in Netlify Functions covers the same Server mode setup from a hosted function.
Why use the SDK instead of calling CapSkip with fetch?
CapSkip speaks the 2captcha API, so fetch against in.php and res.php works. Then you write the polling loop, the back-off and the error mapping yourself. The SDK already polls on a schedule that starts at a quarter of a second, returns the token, and raises ApiException, NetworkException and TimeoutException. The price is two details: the permission errors arrive wrapped, and the SDK sets no socket timeout.
Do other CAPTCHA types work the same way in Deno?
Only reCAPTCHA v2 was run under Deno for this guide. The token types, such as Turnstile and GeeTest, call the same endpoints, so the same two flags should apply, but that was not tested. An image CAPTCHA given as a file path also needs read permission, and one given as a URL needs that site on the net list. Read the options for each type on the Node.js CAPTCHA solver page before you copy a call.
The short version
To solve reCAPTCHA in Deno, put a deno.json with the SDK import in your own folder and import the named export. Read the CapSkip settings yourself. Run with the env list and a net list that holds CapSkip’s address and your site, and nothing more. Tell a denied call from a stopped CapSkip by its cause. Bound the solve with a plain timer, because AbortSignal.timeout holds the process open and a silent CapSkip otherwise hangs it for good.
- Every CAPTCHA type the Node.js package handles: the Node.js CAPTCHA solver page.
- The checkbox reCAPTCHA in plain Node: solving reCAPTCHA v2 in Node.js.
- How the checkbox solver works: the reCAPTCHA v2 solver page.
A script you compile once and schedule can make thousands of solves a month. Running them through an unlimited captcha solver on your own Windows machine means that number never turns into a bill.
