Troubleshooting guide

Feature broke with no code change: upstream service changed or down

A feature that worked yesterday fails today, and your repository has no new commit. The cause is usually a provider that changed an access rule or API version, hit a quota, suspended an account, or had an incident. In our data this pattern ranks 3 of 8 by size, with 121 verified cases.

121 verified cases14 min read Updated 15 Nov 2026Fix it with GemmeinHow we collect cases

What you'll see

  • A feature stops working in production and nothing changed in your code or config.
  • Deploys stay pending, fail on every push, or are blocked by a credit or billing flag while the billing page looks fine.
  • Requests to your own API or functions return an empty 403 or 404 from the hosting edge, or intermittent 502s from an identity endpoint.
  • Health checks are green and logs say the refresh succeeded, but the data on screen is empty or stale.
  • A payments or no-code plugin starts failing after the provider releases a new API version.
  • The whole app goes offline after a trial expires, a card fails or a quota drops, and nobody notices until a user reports it.

Why it happens

Cause 1

The provider changed a rule and you had no signal

An API version, an access rule, a rate limit or a port that switched to TLS can break a client that was only ever tested against a fake or one happy path. Without a timeout, an error-rate alert or a scheduled test against the real service, the first alarm is a user. Stripe, for example, uses your account's default API version for any request that does not set a Stripe-Version header, and webhook events use the version chosen when the endpoint was created.

Cause 2

Account state blocks production while the code is fine

An expired trial, a failed payment method, a stale credit flag or a cut quota can stop production while your code is untouched. Health checks stay green because the app itself is fine. Monitor the billing and quota state of every provider on the critical path as a dependency.

Cause 3

Edge protection blocks your own traffic

On Vercel, a firewall rule with the challenge action only lets requests through inside a valid browser challenge session, so scripts, cURL and server-to-server calls fail. A deny action returns 403 Forbidden and the request never reaches your app. System DDoS mitigation can also challenge or block requests it judges abnormal, including legitimate API traffic from a proxy or shared network.

Cause 4

Errors are swallowed before they reach a dashboard

A job that catches an upstream error, logs 'refreshed' and returns normally makes every dashboard look healthy. A health endpoint that checks your own process says nothing about the dependency. Both hide an outage until the data is visibly wrong.

How to fix it

  1. Find out whether the cause is you or the upstream

    Check the provider's status page, the deployment or commit status, and the provider's changelog for the day it broke. Reproduce the failing call with curl from outside your infrastructure and compare the status code with what your app logs. On Vercel, a deploy hook that fails to create a deployment shows its error in the status check on the associated commit.

    curl -s -o /dev/null -w "%{http_code}\n" --max-time 5 https://api.example.com/health
  2. Put a timeout and a degraded mode on every upstream call

    AbortSignal.timeout() aborts the request after a fixed time and rejects with a TimeoutError. Catch it, log it with the dependency's name, and serve a stale or reduced result instead of hanging. Mark the value as stale so the UI does not present it as fresh.

    async function getPrice(id: string) {
      try {
        const res = await fetch(`https://api.example.com/prices/${id}`, {
          signal: AbortSignal.timeout(3000),
        });
        if (!res.ok) throw new Error(`upstream ${res.status}`);
        return { ...(await res.json()), stale: false };
      } catch (err) {
        console.error("upstream_failed", { dep: "prices", id, err: String(err) });
        return { ...lastKnownPrice(id), stale: true };
      }
    }
  3. Pin the API version and test upgrades before they reach production

    Set the version in code instead of relying on a dashboard default. For Stripe, pass apiVersion to the client and try a newer version in a sandbox first. An existing webhook endpoint keeps the version it was created with, so to move webhooks, create a new endpoint with the target api_version. While both endpoints exist, process events from one of them, then disable the old one.

    import Stripe from "stripe";
    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
      apiVersion: "2026-10-28.endive", // the version you tested
    });
  4. Run a contract test against the real service on a schedule

    Call the live provider with a test credential from CI on a schedule, and assert the shape you depend on: status code, field names and types. A change in format fails this job before it fails a customer. Send the failure to a channel someone reads.

    status=$(curl -s -o /tmp/body.json -w "%{http_code}" --max-time 5 \
      https://api.example.com/v1/prices/test)
    [ "$status" = "200" ] && jq -e '.amount | type == "number"' /tmp/body.json
  5. Make the health check and the alert exercise the real path

    A health endpoint should make one cheap call to each critical dependency and report the result. Alert on the upstream error rate and on stale data, not only on your own 5xx. Count a refresh that returns an empty or unchanged payload as a failure.

  6. Clear account-level and edge-level blockers

    Check billing, trial end dates, credits and quotas for every provider on the critical path, and renew or raise them before launch. On Google Cloud, quotas are managed on the Quotas & System Limits page under IAM & Admin. Raising one needs the Quota Administrator role and a request that is reviewed. On Vercel, add a bypass rule for trusted automation instead of leaving it behind a challenge rule. Pro and Enterprise plans also have System Bypass Rules. 'Pause System Mitigations' lasts 24 hours and leaves you responsible for usage from traffic that would have been blocked.

  7. Recover missed events and keep a fallback for the critical path

    Stripe retries undelivered live-mode events for up to three days with exponential backoff, and can resend one on demand. Make handlers idempotent, because events can arrive more than once and out of order. For hosting, keep a second route ready: during one provider incident, builders pointed DNS at a direct A record. That removes the proxy's protection, so use it as a temporary measure.

    stripe events resend <event_id> --webhook-endpoint=<endpoint_id>

Check it's fixed

  • The scheduled contract test passes against the real service, and a deliberately broken assertion makes it alert within one run.
  • With the upstream blocked (for example, a firewall rule on the outbound host in staging), the feature returns the degraded result within your timeout and the UI marks it stale.
  • The health endpoint goes red when a dependency call fails, and the alert reaches a person.
  • Billing, quota and trial dates for each critical provider are recorded, with a reminder at least a week before each one takes effect.

Fix it with Gemmein

Gemmein names the failure: errors throw a GemmeinError with a code, a request that never reached Gemmein throws network_unreachable (status 0), and some codes are kept as rows on the Logs page rather than returned as an answer. It also keeps checking your domain: a lost receiving MX record is recorded in Logs, and if a domain's TXT record and site CNAME are both gone on three checks in a row, you get one email.

  1. Branch on err.code, and handle network_unreachable separately

    Errors throw a GemmeinError with .status, .code and .message; a request that never reached Gemmein throws network_unreachable with status 0, its message naming the host and err.cause carrying the transport's own error. The docs treat "the server said no" and "the server was never reached" as different problems: branch on them separately, and keep the app usable for the second.

    try {
      await g.subscriptions.checkout("pro");
    } catch (err) {
      if (err.code === "network_unreachable") {
        // the server was never reached: keep the app usable
      } else {
        // the server said no: branch on err.code, render err.message
      }
    }
  2. Read the code's own page

    A refusal in Development, from a secret key, the CLI or the MCP connector also carries .docs, the code's own page at https://docs.gemmein.com/r/research/errors/<code>. The errors page lists every code with its status and fix, including what npx gemmein sync and go-live answer (such as billing_past_due and billing_paused) and the codes that are only rows on the Logs page.

  3. Check whether Stripe is connected and working

    npx gemmein payments --cloud reads back what the cloud has, including each item's Payment Link and whether Stripe is connected and working. When the owner turns payments off, g.subscriptions.checkout and g.payments.buy answer 409 payments_off at once.

    npx gemmein payments --cloud
  4. Check the domain the app depends on

    Press "Check now" on the domain's status card: Gemmein looks at what three public DNS servers see and says, in one line, where the domain stands and the one thing to change; your AI assistant reads the same answer through domain_check and domain_connect_link, and when it needs your DNS state it runs npx gemmein domains and asks you only for records not found yet. A browser calling with your Live key from an address that is not a verified domain is refused 403 origin_not_allowed, naming the address, and Errors lists such addresses (up to five shown).

    npx gemmein domains

Questions

How do I tell a provider outage from a bug in my code?

Check the provider's status page and changelog for the time it broke, then reproduce the call with curl from outside your infrastructure. If curl fails the same way with no code change, the cause is upstream or account state.


Why do my Stripe webhooks or API calls change behavior when I did not deploy?

Requests without a Stripe-Version header use the account's default API version, and webhook events use the version set when the endpoint was created. Pin the version in code and test a new one in a sandbox before moving.


Will Stripe resend webhooks my endpoint missed?

In live mode Stripe retries undelivered events for up to three days with exponential backoff, and in a sandbox it retries three times over a few hours. You can resend an event with the Stripe CLI. Make handlers idempotent.


Why do my API requests get 403 or a challenge page from Vercel?

A firewall rule with the challenge action only passes requests inside a browser challenge session, and a deny action returns 403 Forbidden. Check the Firewall tab for the rule or system mitigation that matched, then add a bypass for trusted traffic.


What should a fallback provider look like?

Test it on a schedule like the primary, put it behind the same interface in your code, and use it for the critical path, such as payments, sign-in or the web route. An untested fallback tends to fail when it is needed.


Sources

← Back to the full report