{"id":26192,"date":"2026-10-01T19:08:51","date_gmt":"2026-10-01T19:08:51","guid":{"rendered":"https:\/\/capskip.com\/?p=26192"},"modified":"2026-10-01T19:08:51","modified_gmt":"2026-10-01T19:08:51","slug":"netlify-functions-captcha","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/netlify-functions-captcha\/","title":{"rendered":"\u5982\u4f55\u5728 Netlify Functions \u4e2d\u8bc6\u522b\u9a8c\u8bc1\u7801\uff08Node.js SDK\uff09"},"content":{"rendered":"<p>A Netlify Functions CAPTCHA solve can&#8217;t run in the synchronous function that answers the request. Netlify stops that function at 60 seconds and won&#8217;t let you raise the limit, while a reCAPTCHA solve can take minutes. Solve in a background function instead, which gets 15 minutes, have it post the form itself, and record the outcome in Netlify Blobs for a second function to report. Point the CapSkip Node.js SDK at your solver in Server mode, because 127.0.0.1 inside a Netlify function is Netlify&#8217;s machine, not yours. Then deal with retries: a background function that throws runs again, and a careless one posts the form twice.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>CapSkip running on a Windows machine you control, in Server mode, with a static public IP and the solver&#8217;s port reachable from the internet. Server mode is under <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a>, and Step 1 covers the rest.<\/li>\n<li>A Netlify site with functions in netlify\/functions, built with Node.js 22.12 or later, which @netlify\/blobs needs. Netlify runs functions on the Node.js version your build uses. Background functions are on every credit-based plan, Free included; legacy plans differ, so check yours.<\/li>\n<li>The capskip package, version 1.3.0 or later, plus @netlify\/blobs for the job records and cheerio to read the page. Inside a function, Blobs needs no setup: Netlify fills in the site and the token for you.<\/li>\n<li>The URL of the page with the CAPTCHA. The samples solve reCAPTCHA v2, and the same shape works for every type CapSkip handles.<\/li>\n<\/ul>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># npm install capskip @netlify\/blobs cheerio\nnpm install capskip @netlify\/blobs cheerio<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Why a synchronous function can&#8217;t do it<\/h2>\n<p>Netlify gives each kind of function a fixed execution limit, listed in <a href=\"https:\/\/docs.netlify.com\/build\/functions\/configuration\/\" rel=\"nofollow noopener\" target=\"_blank\">its functions configuration docs<\/a>. None of the three can be changed:<\/p>\n<table>\n<thead>\n<tr>\n<th>Function type<\/th>\n<th>Execution limit<\/th>\n<th>Its job in this setup<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Synchronous<\/td>\n<td>60 seconds<\/td>\n<td>Report a job&#8217;s result<\/td>\n<\/tr>\n<tr>\n<td>Scheduled<\/td>\n<td>30 seconds<\/td>\n<td>Start a job on a timer<\/td>\n<\/tr>\n<tr>\n<td>Background<\/td>\n<td>15 minutes<\/td>\n<td>Solve, then post the form<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Now compare that with a Netlify Functions CAPTCHA solve. The SDK waits up to recaptchaTimeout, 300 seconds by default, for a reCAPTCHA answer, and CapSkip itself lets a task wait 250 seconds for a free thread and spend another 250 solving. A solve that takes 20 seconds on a quiet afternoon takes 90 when every thread is busy or a proxy is slow. When a synchronous function reaches 60 seconds, Netlify ends it. CapSkip doesn&#8217;t know that, so it works the job to the end and nobody ever collects the answer.<\/p>\n<p>context.waitUntil looks like a way out, since it keeps a function running after the response has gone. It isn&#8217;t one. Netlify&#8217;s docs say the function can still run only until its execution limit, async work included, so a solve handed to waitUntil dies at 60 seconds just the same. Streaming responses share that 60 second limit too.<\/p>\n<p>A background function answers the caller with 202 straight away and keeps running for up to 15 minutes. The price is in that 202: nobody receives the function&#8217;s return value. And a reCAPTCHA token <a href=\"https:\/\/capskip.com\/recaptcha-token-expiration\/\">expires about two minutes after it is issued<\/a>, so it can&#8217;t sit waiting for a caller to come and collect it. The background function has to use the token itself, by posting the form, and leave a record of what happened.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: point the SDK at your solver in Server mode<\/h2>\n<p>Inside a Netlify function, 127.0.0.1 is the function&#8217;s own sandbox. A client built with the defaults connects to nothing there and throws NetworkException with ECONNREFUSED. Switch CapSkip to Server mode so it listens on your public IP, forward the port to the Windows machine if it sits behind a router, and allow the port through Windows Firewall.<\/p>\n<p>Then decide who may connect. By default the addresses Netlify functions connect from change as Netlify scales, so a firewall rule can&#8217;t list them. Netlify&#8217;s Private Connectivity gives functions a fixed set of IPs you can allow, but it is an add-on for Enterprise plans. Without it, the lock is CapSkip&#8217;s API Key Validation setting: switch it on, add a key for this site, and pass it as apiKey. Every SDK request carries the key, over plain HTTP like the rest of the call, so give the function a key of its own that you can delete without breaking anything else.<\/p>\n<p>Store the address and the key as environment variables in Netlify, under Project configuration, then Environment variables, with a scope that includes Functions. Two Netlify rules catch people out here. Variables declared in netlify.toml never reach functions at all. And each deploy keeps the values that were set when it was built, so a new CAPSKIP_HOST does nothing until you deploy again.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">import { CapSkip } from &quot;capskip&quot;;\n\n\/\/ The SDK does not read these by itself, so pass them in.\nif (!process.env.CAPSKIP_HOST) throw new Error(&quot;CAPSKIP_HOST is not set&quot;);\nconst solver = new CapSkip({\n  apiKey: process.env.CAPSKIP_API_KEY,   \/\/ a key from API Key Validation\n  host: process.env.CAPSKIP_HOST,        \/\/ your public IP, Server mode\n  port: Number(process.env.CAPSKIP_PORT ?? 8080),\n});<\/pre>\n<\/div>\n<p>Keep that check. If the variable is missing, host is undefined, and the SDK falls back to its default of 127.0.0.1 without a word, which puts you straight back at ECONNREFUSED. In the full example the check sits inside the solve&#8217;s try block, so a missing variable lands in the job record instead of stopping the function before it can write one.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: solve and post in a background function<\/h2>\n<p>Setting background to true in the function&#8217;s config is all it takes to make one. This one checks a shared secret, because its URL is public and every request to it makes your solver work. Then it fetches the page, reads the sitekey and solves:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ netlify\/functions\/solve-signup.mjs\nimport { getStore } from &quot;@netlify\/blobs&quot;;\nimport * as cheerio from &quot;cheerio&quot;;\n\/\/ solver from Step 1; PAGE_URL is the page with the CAPTCHA.\n\nexport default async (req) =&gt; {\n  if (req.headers.get(&quot;x-job-secret&quot;) !== process.env.JOB_SECRET) return;\n  const { jobId, email } = await req.json();\n  const jobs = getStore({ name: &quot;captcha-jobs&quot;, consistency: &quot;strong&quot; });\n\n  const page = await fetch(PAGE_URL);\n  const $ = cheerio.load(await page.text());\n  const result = await solver.recaptcha($(&quot;.g-recaptcha&quot;).attr(&quot;data-sitekey&quot;), PAGE_URL);\n  \/\/ Then claim the job and post the form, below.\n};\n\nexport const config = { path: &quot;\/api\/solve-signup&quot;, method: &quot;POST&quot;, background: true };<\/pre>\n<\/div>\n<p>The answer for reCAPTCHA is in result.code. Use it at once: with a two minute lifetime, the run that solves is the run that posts.<\/p>\n<p>Now the retries. When a background function ends in an error, Netlify runs it again one minute later, and if that fails too, once more two minutes after that. Before the form is posted, a retry is exactly what you want: a fresh page and a fresh solve. After the form is posted, a retry is a second signup. So the function claims the job in Blobs before it posts:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ Only one run can create this key, so only one run posts.\nconst claim = await jobs.setJSON(`${jobId}-posted`, { at: Date.now() }, { onlyIfNew: true });\nif (!claim.modified) return;\n\nconst form = $(&quot;form&quot;).has(&quot;.g-recaptcha&quot;);\nconst body = new URLSearchParams(form.serializeArray().map((f) =&gt; [f.name, f.value]));\nbody.set(&quot;email&quot;, email);\nbody.set(&quot;g-recaptcha-response&quot;, result.code);\nconst cookie = page.headers.getSetCookie().map((c) =&gt; c.split(&quot;;&quot;)[0]).join(&quot;; &quot;);\n\nconst res = await fetch(new URL(form.attr(&quot;action&quot;) || PAGE_URL, PAGE_URL), {\n  method: &quot;POST&quot;,\n  headers: { cookie, referer: PAGE_URL },\n  body,\n});\nawait jobs.setJSON(jobId, { state: &quot;done&quot;, status: res.status });<\/pre>\n<\/div>\n<p>With onlyIfNew, the write succeeds only if the key does not exist yet, and modified says whether this run created it. Two runs racing for the same job can&#8217;t both get true, so only one of them posts. The function also checks for that key before it starts, so a rerun of a finished job returns without spending a solve.<\/p>\n<p>The post itself sends the page&#8217;s cookies and the form&#8217;s own fields, a hidden CSRF token included, which is what most signup forms check besides the CAPTCHA. It also sends the page as Referer, because fetch sends none and some frameworks refuse an HTTPS form post without it. Once the claim is taken, record any failure in Blobs instead of throwing. A retry would stop at the claim, so throwing buys nothing, and the record is the one place your status endpoint can show the error. The full example below wraps both phases this way.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 3: start jobs and read their results<\/h2>\n<p>Start a job from any server-side code by posting to the background function. The caller makes up the job id, because a 202 has no body to carry one back:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">const jobId = crypto.randomUUID();\nconst start = await fetch(&quot;https:\/\/YOUR_SITE.netlify.app\/api\/solve-signup&quot;, {\n  method: &quot;POST&quot;,\n  headers: { &quot;x-job-secret&quot;: process.env.JOB_SECRET },\n  body: JSON.stringify({ jobId, email: &quot;YOUR_EMAIL&quot; }),\n});\nconsole.log(start.status);   \/\/ 202: accepted, not solved yet<\/pre>\n<\/div>\n<p>A small synchronous function reports the record. It runs in milliseconds, far inside the 60 second limit:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ netlify\/functions\/job-status.mjs\nimport { getStore } from &quot;@netlify\/blobs&quot;;\n\nexport default async (req, context) =&gt; {\n  const jobs = getStore({ name: &quot;captcha-jobs&quot;, consistency: &quot;strong&quot; });\n  const job = await jobs.get(context.params.id, { type: &quot;json&quot; });\n  if (!job) return new Response(&quot;unknown or not started&quot;, { status: 404 });\n  return Response.json(job);\n};\n\nexport const config = { path: &quot;\/api\/jobs\/:id&quot;, method: &quot;GET&quot; };<\/pre>\n<\/div>\n<p>Poll it every few seconds. A job has no record until it finishes or an attempt fails, so a 404 means its first attempt is still running, or that it never started because the secret did not match. Read with strong consistency, as here. Blobs is eventually consistent by default: a new record shows up at once, but an update can take up to 60 seconds to reach every edge location, long enough to show a job that has since finished as still retrying.<\/p>\n<p>To run the job on a timer, reach for a scheduled function, but mind its limit: 30 seconds, half the synchronous one. Let it hand the work to the background function and finish in well under a second:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ netlify\/functions\/nightly-signup.mjs\nexport default async () =&gt; {\n  const res = await fetch(`${process.env.URL}\/api\/solve-signup`, {\n    method: &quot;POST&quot;,\n    headers: { &quot;x-job-secret&quot;: process.env.JOB_SECRET },\n    body: JSON.stringify({ jobId: crypto.randomUUID(), email: &quot;YOUR_EMAIL&quot; }),\n  });\n  console.log(&quot;queued:&quot;, res.status);   \/\/ 202 means accepted, not solved\n};\n\nexport const config = { schedule: &quot;@daily&quot; };<\/pre>\n<\/div>\n<p>URL is one of the read-only variables Netlify gives functions at runtime: your site&#8217;s main address. Scheduled functions fire only on published deploys, not on Deploy Previews or branch deploys.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Full working example<\/h2>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ npm install capskip @netlify\/blobs cheerio\n\/\/ netlify\/functions\/solve-signup.mjs\nimport { getStore } from &quot;@netlify\/blobs&quot;;\nimport * as cheerio from &quot;cheerio&quot;;\nimport { CapSkip } from &quot;capskip&quot;;\n\nconst PAGE_URL = &quot;https:\/\/example.com\/signup&quot;;\n\nexport default async (req) =&gt; {\n  \/\/ Anyone can POST to this URL, so check a shared secret first.\n  if (req.headers.get(&quot;x-job-secret&quot;) !== process.env.JOB_SECRET) return;\n  const { jobId, email } = await req.json();\n  const jobs = getStore({ name: &quot;captcha-jobs&quot;, consistency: &quot;strong&quot; });\n  if (await jobs.get(`${jobId}-posted`)) return;   \/\/ already posted once\n\n  \/\/ Phase 1: fetch and solve. Throwing here is safe: Netlify runs\n  \/\/ the function again after one minute, then two minutes later.\n  let page, $, result;\n  try {\n    if (!process.env.CAPSKIP_HOST) throw new Error(&quot;CAPSKIP_HOST is not set&quot;);\n    const solver = new CapSkip({\n      apiKey: process.env.CAPSKIP_API_KEY,\n      host: process.env.CAPSKIP_HOST,\n      port: Number(process.env.CAPSKIP_PORT ?? 8080),\n    });\n    page = await fetch(PAGE_URL);\n    $ = cheerio.load(await page.text());\n    result = await solver.recaptcha($(&quot;.g-recaptcha&quot;).attr(&quot;data-sitekey&quot;), PAGE_URL);\n  } catch (err) {\n    const prev = await jobs.get(jobId, { type: &quot;json&quot; });\n    const attempt = (prev?.attempt ?? 0) + 1;\n    \/\/ The first run plus two retries: after the third, nothing reruns.\n    const state = attempt &lt; 3 ? &quot;retrying&quot; : &quot;failed&quot;;\n    await jobs.setJSON(jobId, { state, attempt, error: String(err) });\n    throw err;\n  }\n\n  \/\/ Phase 2: post the form once. Claim the job first, so a rerun\n  \/\/ that reaches this line finds the claim taken and stops.\n  const claim = await jobs.setJSON(`${jobId}-posted`, { at: Date.now() }, { onlyIfNew: true });\n  if (!claim.modified) return;\n  try {\n    const form = $(&quot;form&quot;).has(&quot;.g-recaptcha&quot;);\n    const body = new URLSearchParams(form.serializeArray().map((f) =&gt; [f.name, f.value]));\n    body.set(&quot;email&quot;, email);\n    body.set(&quot;g-recaptcha-response&quot;, result.code);\n    const cookie = page.headers.getSetCookie().map((c) =&gt; c.split(&quot;;&quot;)[0]).join(&quot;; &quot;);\n    const res = await fetch(new URL(form.attr(&quot;action&quot;) || PAGE_URL, PAGE_URL), {\n      method: &quot;POST&quot;,\n      headers: { cookie, referer: PAGE_URL },\n      body,\n    });\n    await jobs.setJSON(jobId, { state: &quot;done&quot;, status: res.status });\n  } catch (err) {\n    \/\/ No rethrow: a retry could not post again, so record it here.\n    await jobs.setJSON(jobId, { state: &quot;failed&quot;, error: String(err) });\n  }\n};\n\nexport const config = { path: &quot;\/api\/solve-signup&quot;, method: &quot;POST&quot;, background: true };<\/pre>\n<\/div>\n<p>Set JOB_SECRET, CAPSKIP_HOST, CAPSKIP_API_KEY and, if you changed it, CAPSKIP_PORT in the Netlify UI, deploy, and start a job as in Step 3. Every option the reCAPTCHA call takes, invisible and Enterprise included, works here unchanged; <a href=\"https:\/\/capskip.com\/recaptcha-v2-solver\/\">the reCAPTCHA v2 solver page<\/a> covers what the type needs.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Common errors and what they mean<\/h2>\n<table>\n<thead>\n<tr>\n<th>What you see<\/th>\n<th>Cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>The request fails after about a minute and no form is posted<\/td>\n<td>The solve runs in a synchronous function, or inside waitUntil, which shares its 60 second limit<\/td>\n<td>Move the solve into a background function<\/td>\n<\/tr>\n<tr>\n<td>A job record saying CAPSKIP_HOST is not set, or NetworkException with ECONNREFUSED 127.0.0.1:8080 if you dropped that check<\/td>\n<td>CAPSKIP_HOST is missing in the deployed function, so the SDK would fall back to loopback<\/td>\n<td>Set the variable with a scope that includes Functions, then deploy again<\/td>\n<\/tr>\n<tr>\n<td>Works with netlify dev, fails once deployed<\/td>\n<td>Locally the function runs on your own machine, where loopback and your local IP reach the solver<\/td>\n<td>Use Server mode and your public IP for the deployed site<\/td>\n<\/tr>\n<tr>\n<td>A variable from netlify.toml is undefined in the function<\/td>\n<td>Variables declared in netlify.toml never reach functions<\/td>\n<td>Set it in the Netlify UI, CLI or API instead<\/td>\n<\/tr>\n<tr>\n<td>The function still uses an old CAPSKIP_HOST<\/td>\n<td>A deploy keeps the values that were set when it was built<\/td>\n<td>Deploy again after changing a variable<\/td>\n<\/tr>\n<tr>\n<td>A connection that hangs, then NetworkException with ETIMEDOUT<\/td>\n<td>The port is not forwarded, or Windows Firewall drops it<\/td>\n<td>Forward the port, allow it through Windows Firewall, and test it from outside your network<\/td>\n<\/tr>\n<tr>\n<td>ApiException saying ERROR_KEY_DOES_NOT_EXIST<\/td>\n<td>API Key Validation is on and the key is not in CapSkip&#8217;s list, or CAPSKIP_API_KEY is unset and the SDK sent its default<\/td>\n<td>Add the key in CapSkip and set the variable<\/td>\n<\/tr>\n<tr>\n<td>The form was posted twice<\/td>\n<td>An error after the post triggered a retry, and nothing stopped the second run<\/td>\n<td>Claim the job with onlyIfNew before posting, as in Step 2<\/td>\n<\/tr>\n<tr>\n<td>The status endpoint returns 404 forever<\/td>\n<td>The secret did not match, so the function returned before writing anything<\/td>\n<td>Set the same JOB_SECRET in the caller and the site<\/td>\n<\/tr>\n<tr>\n<td>The site rejects the token<\/td>\n<td>It was older than about two minutes, or already used<\/td>\n<td>Post straight after solving, one token per post<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">FAQ<\/h2>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Can I raise the 60 second limit for a Netlify Functions CAPTCHA solve?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">No. Netlify lists the synchronous, scheduled and background limits as fixed, and waitUntil and streaming responses stay inside the 60 seconds. The 15 minute background limit is the long one on offer, and it covers the SDK&#8217;s 300 second wait with plenty to spare.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Can a Netlify function reach CapSkip on my home or office PC?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes, through Server mode. CapSkip listens on your public IP, your router forwards the port to that PC, and the function connects over the same HTTP API it would use locally. A static public IP keeps CAPSKIP_HOST valid between deploys. Without Private Connectivity you can&#8217;t allow Netlify by address, so API Key Validation does the gatekeeping.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Does CapSkip charge per solve when Netlify calls it?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">No. Server mode changes where the solver is reachable from, not who runs it: it is still your own Windows machine, and it doesn&#8217;t count solves. Netlify meters function run time, so a background function that waits two minutes for a solve uses two minutes of it.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">How is this different from running it on AWS Lambda?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">The constraints differ. On Lambda behind API Gateway the wall is a 29 second integration timeout, and a NAT gateway with an Elastic IP gives every solve one fixed source address for your firewall. On Netlify the wall is 60 seconds, the background function is the built-in way around it, and there is no fixed address short of an Enterprise add-on. The Lambda setup is in <a href=\"https:\/\/capskip.com\/aws-lambda-captcha\/\">the AWS Lambda CAPTCHA guide<\/a>.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>For a Netlify Functions CAPTCHA job, never solve in a synchronous function: its 60 second limit is fixed, and waitUntil doesn&#8217;t escape it. Solve in a background function, post the form in the same run while the token is fresh, and claim the job with an onlyIfNew write first so Netlify&#8217;s two retries can never post twice. Report results from Blobs through a small synchronous function. Connect to CapSkip in Server mode, with the address and key in Functions-scoped variables and API Key Validation switched on.<\/p>\n<ul>\n<li>Every other CAPTCHA type the Node package solves: <a href=\"https:\/\/capskip.com\/nodejs-captcha-solver\/\">the Node.js CAPTCHA solver page<\/a>.<\/li>\n<\/ul>\n<p>Netlify supplies the functions, and the solving stays on a Windows machine you own. That is the point of running your own <a href=\"https:\/\/capskip.com\/\">captcha solver<\/a>: the platform&#8217;s meter counts minutes, and nothing counts solves.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Netlify stops a synchronous function at 60 seconds and lets no one raise it, while a solve can take minutes. Here is the background function that solves and posts in one run, the Blobs claim that makes Netlify&#8217;s retries safe, and the Server mode setup it connects through.<\/p>","protected":false},"author":1,"featured_media":26191,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Netlify Functions CAPTCHA: Beat the 60s Limit | CapSkip","rank_math_description":"A Netlify Functions CAPTCHA solve dies at the 60 second limit. Solve in a background function, guard its retries with Blobs, and connect in Server mode.","rank_math_focus_keyword":"netlify functions captcha","footnotes":""},"categories":[70],"tags":[],"class_list":["post-26192","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-captcha"],"_links":{"self":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/26192","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/comments?post=26192"}],"version-history":[{"count":2,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/26192\/revisions"}],"predecessor-version":[{"id":26196,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/26192\/revisions\/26196"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/26191"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=26192"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=26192"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=26192"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}