Home / Blog / Amazon Product API on Google Cloud Functions (2nd Gen)

Amazon Product API on Google Cloud Functions (2nd Gen)

Deploy an Amazon product lookup as a Google Cloud Functions (2nd gen) HTTP function by reusing the TypeScript quickstart's client.js, retry.js and lookup.js unmodified. Two GCP-specific traps matter here: a brand-new function's --timeout defaults to 60 seconds even though the ceiling is 3600, and any logging must be awaited before the response is sent, since code afterward is not guaranteed to run.

November 11, 2026
6 min read
SellerMagnet Team
Share & Bookmark
Terminal showing a Google Cloud Function HTTP handler calling the SellerMagnet product lookup API

To call an Amazon product API from Google Cloud Functions, deploy a 2nd gen HTTP function that imports the SellerMagnet TypeScript quickstart's own client.js, retry.js and lookup.js unchanged, and set --timeout=130s explicitly: a new function's timeout defaults to 60 seconds even on 2nd gen, which is shorter than SellerMagnet's own up to 120-second scrape allowance.

Key Takeaways

  • A brand-new Cloud Function's --timeout defaults to 60 seconds even on 2nd gen, whose ceiling is 3600 seconds for an HTTP trigger; the low default, not the ceiling, is what actually breaks a first deploy of this proxy.
  • Cloud Functions counts the FULL wait against that timeout, the same shape as AWS Lambda; unlike Cloudflare Workers, which meter CPU time only, waiting on SellerMagnet's up to 120-second allowance is not free here.
  • Code run after the HTTP response is sent is not guaranteed to execute, per Google's own documentation; this handler awaits its request log write before res.json(), never after.
  • A malformed ASIN or marketplace ID is rejected inside the handler before any call is made, so it never spends a credit.
  • Unlike the Lambda version of this proxy, a Cloud Functions HTTP trigger has no separate API-Gateway-style integration timeout in front of it, so only the function's own --timeout needs raising.

Why does the default deploy time out before SellerMagnet answers?

Google's own gcloud functions deploy reference states it plainly: --timeout "defaults to original value for existing function or 60 seconds for new functions", and for 2nd gen functions "cannot be more than 3600s". A fresh deploy of this proxy, left at its default, gives SellerMagnet only 60 seconds to answer, less than half of SellerMagnet's own up to 120-second scrape allowance (already documented in the quickstart series), so a slower lookup is killed by GCP's own DEADLINE_EXCEEDED long before SellerMagnet would have answered. The fix is one explicit flag at deploy time, not a code change.

Request-duration limits that matter for this proxy (checked live, Nov 2026).
LimitAWS Lambda (this series)GCP Cloud Functions (2nd gen, HTTP)Why it matters here
Default --timeout (new function)3 s60 sShorter than SellerMagnet's own up to 120 s wait
Max --timeout (configurable)900 s (15 min), all triggers3600 s (60 min), HTTP-triggered onlyGCP's ceiling is higher; the LOW default is the real trap
API Gateway integration cap (if fronted by one)50 ms-29 s, fixed, not raisableno equivalent for a Cloud Functions HTTP triggerGCP's HTTP trigger has its own URL, nothing in front of it

deploy.sh - the flag that must be set explicitly

# deploy.sh - deploy flags for the handler below. --timeout MUST be set explicitly:
# a new function defaults to 60s even on 2nd gen (max 3600s for an HTTP trigger), and
# SellerMagnet's own scrape allowance can run up to ~120s.
gcloud functions deploy amazon-lookup \
  --gen2 \
  --runtime=nodejs20 \
  --region=us-central1 \
  --source=. \
  --entry-point=lookupHandler \
  --trigger-http \
  --timeout=130s \
  --set-env-vars=SELLERMAGNET_API_KEY=your-key-here \
  --allow-unauthenticated   # or add IAM auth in front, this flag only controls invocation auth

Reusing client.js, retry.js and lookup.js unmodified

Cloud Functions' Node.js runtime reads environment variables the ordinary Node.js way, set with --set-env-vars or pulled from Secret Manager, with no special compatibility flag the way Cloudflare Workers need one. That means the TypeScript quickstart's client.js, which reads process.env.SELLERMAGNET_API_KEY directly, needs no edits at all; retry.js is copied in exactly as published, and lookup.js is adapted to inline its one decodePrice helper instead of importing a separate types.js, since this proxy intentionally ships three files, not four.

client.js - unmodified from the TypeScript quickstart

// client.js - one fetch-based client, X-Api-Key header, 120 s timeout.

// Overridable only for pointing the client at a local test server; a real
// program never needs to set SELLERMAGNET_API_BASE.
const API_BASE = process.env.SELLERMAGNET_API_BASE ?? "https://sellermagnet-api.com/api/";

/**
 * @typedef {Object} ApiErrorShape
 * @property {number} status
 * @property {string} message
 * @property {number} retryAfter - seconds, 0 when the header was absent.
 */

/** Thrown for any non-2xx response or a failed JSON decode. */
export class ApiError extends Error {
  /** @param {ApiErrorShape} shape */
  constructor({ status, message, retryAfter }) {
    super(`${status}: ${message}`);
    this.name = "ApiError";
    this.status = status;
    this.retryAfter = retryAfter;
  }
}

/**
 * Send one GET to the API and return the decoded `data` field.
 * @template T
 * @param {string} endpoint - e.g. "amazon-product-lookup"
 * @param {Record<string, string>} params
 * @returns {Promise<T>}
 */
export async function get(endpoint, params) {
  const url = new URL(API_BASE + endpoint);
  for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);

  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 120_000); // server allows up to ~120 s
  let response;
  try {
    response = await fetch(url, {
      headers: { "X-Api-Key": process.env.SELLERMAGNET_API_KEY ?? "" },
      signal: controller.signal,
    });
  } finally {
    clearTimeout(timeout);
  }

  const retryAfter = Number.parseInt(response.headers.get("retry-after") ?? "0", 10) || 0;
  let body;
  try {
    body = await response.json(); // a proxy's error page is HTML, not JSON
  } catch {
    throw new ApiError({ status: response.status, message: response.statusText, retryAfter });
  }
  if (response.ok && body.success) return body.data;
  throw new ApiError({ status: response.status, message: body.message ?? response.statusText, retryAfter });
}

retry.js - unmodified from the TypeScript quickstart

// retry.js - retries that respect billing: 429/500(once)/502/503 only.

import { ApiError } from "./client.js";

const RETRYABLE = new Set([429, 500, 502, 503]);

/**
 * @template T
 * @param {() => Promise<T>} call
 * @param {number} [attempts]
 * @returns {Promise<T>}
 */
export async function withRetry(call, attempts = 4) {
  let seen500 = false;
  for (let attempt = 1; ; attempt++) {
    try {
      return await call();
    } catch (err) {
      if (!(err instanceof ApiError)) throw err;
      const give500Twice = err.status === 500 && seen500;
      if (!RETRYABLE.has(err.status) || give500Twice || attempt === attempts) throw err;
      seen500 = seen500 || err.status === 500;
      const backoff = 2 ** attempt * 100; // ms, small for the test harness
      const wait = Math.max(backoff, err.retryAfter * 1000);
      await new Promise((resolve) => setTimeout(resolve, wait));
    }
  }
}

lookup.js - inlines decodePrice; no separate types.js is shipped

// lookup.js - typed product lookup; buyBoxInfo.price decoded to number|null.

import { get } from "./client.js";

/**
 * Decode a lookup/offers price field into a number or null.
 * @param {number|string|null|undefined} raw
 * @returns {number|null}
 */
export function decodePrice(raw) {
  return typeof raw === "number" ? raw : null;
}

/**
 * @param {string} asin
 * @param {string} marketplaceId
 */
export async function lookupProduct(asin, marketplaceId) {
  const data = await get("amazon-product-lookup", { asin, marketplaceId });
  const info = data.productInfo;
  return {
    asin: info.asin,
    title: info.title,
    link: info.link,
    buyBoxInfo: {
      price: decodePrice(info.buyBoxInfo.price),
      currencyCode: info.buyBoxInfo.currencyCode,
      sellerId: info.buyBoxInfo.sellerId,
    },
  };
}

The function's own HTTP handler

index.js is the only new file, using the classic functions-framework (req, res) signature. It parses asin and marketplaceId from the query string, rejects a malformed value before any call is made, calls the quickstart's own lookupProduct through withRetry, and awaits a per-request log write before responding, since Google's own docs warn that anything run after the response is not guaranteed to execute.

index.js - the only new file: a thin HTTP handler

// index.js - Google Cloud Functions (2nd gen, HTTP trigger) entry point.
// Imports client.js/retry.js/lookup.js from the TypeScript quickstart (amazon-product-api-
// typescript.py): client.js and retry.js are UNCHANGED; lookup.js is adapted to inline its
// one decodePrice helper instead of importing a separate types.js, since this proxy
// intentionally ships three files, not four.
//   gcloud functions deploy amazon-lookup --gen2 --runtime=nodejs20 --region=us-central1 \
//     --trigger-http --entry-point=lookupHandler --timeout=130s --allow-unauthenticated
// --timeout MUST be set explicitly above 60: new functions default to a 60-second
// timeout even on gen2 (max 3600s for HTTP functions), and SellerMagnet's own scrape
// allowance (up to ~120 s) is itself what the wait is spent on, so the platform's own
// default would kill this proxy with a DEADLINE_EXCEEDED well before SellerMagnet answers
// (facts-additions.md #1).

import { lookupProduct } from "./lookup.js";
import { withRetry } from "./retry.js";
import { ApiError } from "./client.js";

const ASIN_RE = /^[A-Z0-9]{10}$/;
const MARKETPLACE_RE = /^A[A-Z0-9]{9,12}$/; // Amazon marketplace IDs, e.g. ATVPDKIKX0DER

/**
 * Write one line to the per-request ledger. Awaited and completed BEFORE the HTTP
 * response is sent: Cloud Functions gives no guarantee that code after res.json()/
 * res.send() runs at all, since CPU access can be withdrawn the moment the response is
 * delivered (facts-additions.md #2) - so any logging that must happen goes here, not
 * after the response.
 * @param {string} line
 * @param {{write: (line: string) => Promise<void>}} ledger
 */
async function logBeforeResponding(line, ledger) {
  await ledger.write(line);
}

/**
 * Cloud Functions (2nd gen) HTTP entry point: GET /?asin=...&marketplaceId=...
 * @param {{query: Record<string, string>}} req
 * @param {{status: (code: number) => any, json: (body: any) => void}} res
 * @param {{write: (line: string) => Promise<void>}} ledger - injected for the test harness;
 *   a real deploy uses the default console-backed ledger (see deploy.js).
 */
export async function lookupHandler(req, res, ledger) {
  const asin = (req.query.asin ?? "").toUpperCase();
  const marketplaceId = req.query.marketplaceId ?? "";

  // Reject a malformed ASIN or marketplace ID before any call is made: a bad shape can
  // never succeed, so failing closed here never spends a credit (same convention as
  // every other quickstart's pre-flight check).
  if (!ASIN_RE.test(asin)) {
    res.status(400).json({ error: `"${asin}" is not a 10-character ASIN - rejected, no credit spent` });
    return;
  }
  if (!MARKETPLACE_RE.test(marketplaceId)) {
    res.status(400).json({ error: `"${marketplaceId}" is not a marketplace ID - rejected, no credit spent` });
    return;
  }

  try {
    // withRetry caps at 4 attempts, same series-wide billing-aware policy.
    const product = await withRetry(() => lookupProduct(asin, marketplaceId));
    await logBeforeResponding(`${asin} 200 price=${product.buyBoxInfo.price}`, ledger);
    res.status(200).json({
      asin: product.asin, title: product.title, link: product.link,
      price: product.buyBoxInfo.price, currencyCode: product.buyBoxInfo.currencyCode,
    });
  } catch (err) {
    if (err instanceof ApiError) {
      // Pass through the API's own status for anything retry.js already gave up on
      // (404 not found, 401/403 key or credit problems, an exhausted 429); only a
      // genuine upstream failure (500/502/503) is reported to the caller as 502.
      const status = err.status >= 500 ? 502 : err.status;
      await logBeforeResponding(`${asin} ${status} ${err.message}`, ledger);
      res.status(status).json({ error: err.message });
      return;
    }
    throw err;
  }
}

Is it safe to log a lookup's outcome after responding, to shave off latency?

No. Google's "Tips & Tricks" guidance for Cloud Functions and its own "Running effective Node.js apps on Cloud Functions" post both say the same thing in different words: a function has access to CPU only for the duration of its execution, and code run outside that period "is not guaranteed to execute, and it can be stopped at any time". A fire-and-forget ledger.write() called after res.json() can simply never finish, and on a warm instance its leftover async work can resume and interfere with the NEXT invocation. This proxy awaits the write first, so the response always reflects a completed log line.

Status codes, billing and the handler's reaction (checked live, Nov 2026).
StatusMeaningBilledHandler action
200SuccessYesUse data
400 (bad asin/marketplace)Rejected before any callNoFix the input
401 / 403Key or creditsNoStop, status passed through
404Not on this marketplaceYesStore as missing, status passed through
429Concurrency capNoWait Retry-After, retried
500Internal errorNo, refundedRetry once
502 / 503Upstream / breakerYesWait, then retry, capped at 4, reported as 502
Table comparing AWS Lambda and Google Cloud Functions request-duration limits for an Amazon API proxy
GCP's ceiling is far higher than Lambda's, but its low default is the real trap on a first deploy.
Sequence diagram of a Cloud Function awaiting a log write before sending its HTTP response
The log write completes before res.json(); nothing is fired off after the response.

Real output from the tested harness (fakefetch.mjs, illustrative data)

happy: calls=1 order=before-response>response status=200 body={"asin":"B0CL61F39H","title":"t","link":"x","price":444.99,"currencyCode":"USD"}
no-offer-na: calls=1 order=before-response>response status=200 body={"asin":"B0NOOFFER1","title":"t","link":"x","price":null,"currencyCode":"USD"}
bad-asin: calls=0 order=response status=400 body={"error":"\"BAD-ASIN\" is not a 10-character ASIN - rejected, no credit spent"}
bad-marketplace: calls=0 order=response status=400 body={"error":"\"not-a-marketplace\" is not a marketplace ID - rejected, no credit spent"}
not-found-404: calls=1 order=before-response>response status=404 body={"error":"404: Product not found"}
rate-limited-429: calls=2 order=before-response>response status=200 body={"asin":"B0RETRY429","title":"t","link":"x","price":19.99,"currencyCode":"USD"}
internal-error-500: calls=2 order=before-response>response status=200 body={"asin":"B0RETRY500","title":"t","link":"x","price":29.99,"currencyCode":"USD"}
gateway-errors-capped: calls=4 order=before-response>response status=502 body={"error":"503: breaker open"}
unauthorized-401: calls=1 order=before-response>response status=401 body={"error":"401: Invalid or inactive API key."}
log-before-response-order: calls=1 order=before-response>response status=200 body={"asin":"B0CL61F39H","title":"t","link":"x","price":9.99,"currencyCode":"USD"}
order-check: ["before-response","response"] expected=["before-response","response"]
---
ALL 10 SCENARIOS PASS (10/10 assertions, calls counted per scenario above)

Frequently Asked Questions

Why does a Cloud Function time out even though SellerMagnet answered inside 120 seconds?

Because gcloud functions deploy defaults a brand-new function's --timeout to 60 seconds, even on 2nd gen (whose ceiling is 3600 seconds for an HTTP trigger). The platform enforces its own low default, not SellerMagnet's allowance, so --timeout must be set explicitly.

Is the 120-second wait as cheap on Cloud Functions as it is on Cloudflare Workers?

No. Cloudflare only meters CPU time, so waiting on fetch() is free there. Cloud Functions, like AWS Lambda, bills and times out on the FULL wall-clock duration, so the function's own configured timeout, not just its CPU usage, must cover the wait.

Can I log the outcome of a lookup after sending the HTTP response, to save latency?

Not reliably. Google's own docs state that code run after the response is sent is not guaranteed to execute and can be stopped at any time, since CPU access can be withdrawn the instant the response is delivered. Await anything that must happen before res.json().

Do I need to edit the TypeScript quickstart's client.js to use it on Cloud Functions?

No. Cloud Functions' Node.js runtime populates process.env from --set-env-vars or Secret Manager the normal Node.js way, with no special compatibility flag, so the unmodified client.js (which reads process.env.SELLERMAGNET_API_KEY) works as-is.

How is this different from the AWS Lambda version of this proxy?

Lambda's own timeout can reach 900 seconds, but a Lambda sitting behind API Gateway is further capped at a fixed, non-raisable 29-second integration timeout, which is why that guide uses a Function URL instead. A Cloud Functions HTTP trigger has no separate gateway layer in front of it, so only the function's own --timeout matters, once it is raised.

Which Node.js runtime should I deploy with?

nodejs18 or later: global fetch and AbortController, which client.js relies on, ship without a package starting there (same minimum as the TypeScript quickstart); the deploy example below uses nodejs20 simply as GCP's current recommended runtime.

Bottom line: Google Cloud Functions can run this proxy with zero changes to the TypeScript quickstart's client.js, retry.js and lookup.js, provided --timeout is raised explicitly above the platform's own 60-second default and any logging is awaited before the response is sent. That combination of "the full wait counts, like Lambda" plus "the default is just low, unlike Lambda's fixed API-Gateway cap" is what makes this platform's failure mode different from either of the other two serverless versions of this proxy. Every endpoint is documented on the API documentation page, and more runnable snippets are on the code examples page; a free account includes 150 credits to try it.

Ready to Extract Amazon Data at Scale?

Start building with SellerMagnet API today. Real-time product data, competitive pricing, and review analytics at your fingertips.

150 free API credits • No credit card required • Cancel anytime