When calling an API, 401 Unauthorized, 403 Forbidden, and 429 Too Many Requests are the three most common types of errors.

They all belong to 4xx client errors, but their solutions are completely different:

  • 401: Authentication failed—the server cannot confirm “who you are”
  • 403: Access denied—the server understands the request, but does not allow it to be executed
  • 429: Too many requests—the calling speed or usage has exceeded the limit

If you retry directly without distinguishing the error type, not only will it fail to solve the problem, but it may also create a retry storm, and even cause the API to be further restricted.

This article will explain how to handle these three types of errors through troubleshooting checklists, curl commands, and Node.js code.

Understand 401, 403, and 429 at a Glance

Status Code Core Meaning Common Causes Should You Retry Immediately
401 Unauthorized Missing valid authentication credentials Incorrect API Key, expired Token, incorrect authentication header format Should not retry unchanged
403 Forbidden The server refuses to process the request Insufficient permissions, IP or regional restrictions, resource policies, WAF blocking Usually should not
429 Too Many Requests Requests exceed rate or quota limits Excessive concurrency, QPS limit exceeded, quota exhausted Retry after waiting

It should be noted that although the English name of 401 is Unauthorized, in actual troubleshooting, it is more appropriate to understand it as “not authenticated”; 403 is usually related to authorization or access policies.

I. 401 Unauthorized: Check Authentication Credentials First

HTTP 401: API credentials are missing, invalid, or expired

HTTP 401 Unauthorized indicates that the request lacks the valid authentication credentials required by the target resource. A standard 401 response usually tells the client the authentication scheme expected by the server through the WWW-Authenticate response header.

Common Causes

  1. The Authorization request header was not sent
  2. The API Key or Access Token is incorrect
  3. The Token has expired or been revoked
  4. The authentication scheme is incorrect, for example, Bearer is missing
  5. Environment variables were not loaded correctly
  6. A key for the wrong environment was used
  7. The authentication header was lost when the request was forwarded by a proxy or gateway
  8. The time difference between the client and server is too large, causing the signature to become invalid

Common Errors

The API requires a Bearer Token:

Authorization: Bearer YOUR_ACCESS_TOKEN

But the client sends:

Authorization: YOUR_ACCESS_TOKEN

Or incorrectly writes:

Authentication: Bearer YOUR_ACCESS_TOKEN

Both cases may return 401.

Troubleshooting with curl

Do not only inspect the response body at the beginning. Add -i and check both the status code and response headers:

curl -i "https://api.example.com/v1/profile" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Accept: application/json"

Focus on checking:

  • Whether Authorization was actually sent
  • Whether the authentication scheme is correct
  • Whether there are spaces or line breaks before or after the Token
  • What WWW-Authenticate returns
  • Whether there is error information such as invalid_token or expired_token in the response body

To view a more detailed request process, you can use:

curl -v "https://api.example.com/v1/profile" \
  -H "Authorization: Bearer $API_TOKEN"

Note: Debug logs may contain Tokens. Do not directly send complete logs to public Issues, group chats, or tickets.

Should 401 Be Retried?

Do not repeatedly retry using the same invalid credentials.

Recommended process:

  1. Check whether the request includes authentication information
  2. Check whether the Token has expired, been revoked, or copied incorrectly
  3. If Refresh Token is supported, refresh the access token once
  4. Retry once with the new Token
  5. If it still returns 401, stop retrying and log the error

If a fixed API Key is used, you should check the key status or replace it with a valid key, rather than retrying in a loop.

II. 403 Forbidden: Authentication May Have Succeeded, but the Request Is Still Rejected

HTTP 403: The request is denied due to permissions or access policies

403 Forbidden indicates that the server has understood the request, but refuses to process it. For typical permission errors, even re-authentication will not automatically solve the problem; if the request or permission configuration is not changed, repeated requests will usually still fail.

Common Causes

  • The current account or key lacks the required permissions
  • The Token's Scope is insufficient
  • Accessing a resource that does not belong to the current project or tenant
  • The source IP is not in the allowlist
  • The request source region is restricted
  • Blocked by WAF, firewall, or anti-abuse systems
  • The account, project, or resource has been disabled
  • The API is only available to specific plans or roles
  • The signature is valid, but the resource scope in the signature does not match
  • Restricted by CSRF, Origin, or Referer policies in browser scenarios

The Difference Between 401 and 403

You can quickly distinguish them with two sentences:

  • 401: The server cannot confirm who you are
  • 403: The server knows or understands your identity and request, but does not allow you to do this

For example, an ordinary member uses a valid Token to call the “delete organization” API:

curl -i -X DELETE \
  "https://api.example.com/v1/organizations/org_123" \
  -H "Authorization: Bearer $API_TOKEN"

If the Token is valid but the account is not an organization administrator, the server may return:

HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "error": "insufficient_permissions",
  "message": "Administrator role is required."
}

403 Troubleshooting Order

1. Read the Response Body

Prioritize looking for error codes, missing roles or permissions, required Scopes, denied resource IDs, and IP, region, or security policy notices.

2. Check the Token Scope

For example, reading data may only require data:read, but creating data may require data:write. A valid Token does not mean it has all permissions.

3. Check Resource Ownership

Confirm that the project ID, organization ID, tenant ID, user ID, region, or environment and the current credentials belong to the same account or authorization scope.

4. Check Network and Security Policies

If the code runs normally locally but returns 403 after deployment, focus on checking the server outbound IP, IP allowlist, proxy configuration, CDN, WAF, request source region, and related request headers.

Do not draw conclusions based on the status code alone

API implementations in the real world are not necessarily completely consistent. Some platforms may return 403 instead of 429 when rate limiting is triggered. For example, the GitHub REST API may return 403 or 429 when the rate limit is exceeded, and indicate the limit status through response headers such as x-ratelimit-remaining, x-ratelimit-reset, or retry-after.

Therefore, when encountering 403, also check comprehensively:

  • Business error codes in the response body
  • Retry-After
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
  • Official platform documentation

Should 403 be retried?

For 403 caused by insufficient permissions, IP restrictions, or resource policies, retrying as-is is meaningless.

The correct approach is to modify permissions or Token Scope, use the correct account or project, adjust allowlists and security policies, correct the resource ID, and confirm whether the service or plan supports the API. Only when the platform explicitly states that the 403 actually represents temporary rate limiting should you wait and retry according to the rate-limiting rules.

3. 429 Too Many Requests: Slow Down First, Then Retry

HTTP 429: API requests are rate-limited; wait and retry with backoff

429 Too Many Requests indicates that the client has sent too many requests within a period of time, meaning the rate limit has been triggered. The server can use Retry-After to tell the client how long it should wait before retrying.

Common causes

  • Too many requests within a short period of time
  • Too many concurrent tasks
  • Multiple services sharing the same API Key
  • Retry code has no waiting mechanism
  • Scheduled tasks run in a concentrated manner at the same time
  • Requests are not cached, repeatedly fetching the same data
  • The quota for a single account, IP, or project has been exhausted
  • Plan usage or balance limitations

First check the response headers

curl -i "https://api.example.com/v1/resources" \
  -H "Authorization: Bearer $API_TOKEN"

Focus on:

Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1710000000

Response header names may differ across platforms; refer to the platform documentation.

Correctly handle Retry-After

The two common formats of Retry-After are seconds or an HTTP date:

Retry-After: 30
Retry-After: Wed, 21 Oct 2026 07:28:00 GMT

Clients should preferably support both formats.

Why can’t you retry immediately?

If you retry immediately after a request is rate-limited, the new request will still be rate-limited; multiple clients may also retry simultaneously, creating a “thundering herd.” A more reliable approach is:

  1. Prioritize following Retry-After
  2. When there is no explicit waiting time, use exponential backoff
  3. Add random jitter to the waiting time
  4. Set a maximum number of retries
  5. Return a clear error after reaching the limit

Node.js: Safely Handle 429

The example below uses Node.js built-in fetch, prioritizing the parsing of Retry-After; if the server does not provide this response header, it uses exponential backoff with random jitter.

const sleep = (ms) =>
  new Promise((resolve) => setTimeout(resolve, ms));

function parseRetryAfter(value) {
  if (!value) return null;

  const seconds = Number(value);
  if (Number.isFinite(seconds) && seconds >= 0) {
    return seconds * 1000;
  }

  const timestamp = Date.parse(value);
  if (!Number.isNaN(timestamp)) {
    return Math.max(0, timestamp - Date.now());
  }

  return null;
}

async function fetchWithRetry(url, options = {}, config = {}) {
  const {
    maxRetries = 3,
    baseDelayMs = 1000,
    maxDelayMs = 30_000,
  } = config;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, options);

    if (response.status !== 429) {
      return response;
    }

    if (attempt === maxRetries) {
      throw new Error(
        `请求持续受到限流,已达到最大重试次数:${maxRetries}`
      );
    }

    const retryAfterMs = parseRetryAfter(
      response.headers.get("retry-after")
    );

    const backoffCap = Math.min(
      maxDelayMs,
      baseDelayMs * 2 ** attempt
    );

    const jitterMs = Math.floor(Math.random() * backoffCap);
    const delayMs = retryAfterMs ?? jitterMs;

    console.warn(
      `收到 429,第 ${attempt + 1} 次等待 ${delayMs}ms 后重试`
    );

    await sleep(delayMs);
  }

  throw new Error("请求失败");
}

Usage example:

const response = await fetchWithRetry(
  "https://api.example.com/v1/resources",
  {
    headers: {
      Authorization: `Bearer ${process.env.API_TOKEN}`,
      Accept: "application/json",
    },
  },
  {
    maxRetries: 3,
    baseDelayMs: 1000,
    maxDelayMs: 30_000,
  }
);

if (!response.ok) {
  const errorBody = await response.text();
  throw new Error(`API 请求失败:${response.status} ${errorBody}`);
}

const data = await response.json();
console.log(data);

When retrying POST requests, idempotency must also be considered

Not all requests are suitable for automatic retries. Read requests such as GET and HEAD are usually easier to retry safely. For POST requests such as creating orders, charging payments, or sending messages, the client may have successfully submitted the request but did not receive a response due to a network interruption. Retrying directly at this point may cause duplicate operations.

If the platform supports it, set an idempotency key for write operations:

curl -i -X POST "https://api.example.com/v1/orders" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-20260331-0001" \
  -d '{
    "product_id": "prod_123",
    "quantity": 1
  }'

The request header name and validity period of the idempotency key are determined by the specific platform; refer to the corresponding API documentation.

Production Environment Troubleshooting Checklist

Received 401

  • Whether the request includes an authentication header
  • Whether the authentication scheme such as Bearer is correct
  • Whether the Token has expired, been revoked, or copied incorrectly
  • Whether a key from the correct environment is being used
  • Whether a proxy or gateway has removed the authentication header
  • Whether the client time is accurate
  • Whether it is safe to refresh the Token once

Received 403

  • Whether the Token has the required Scope
  • Whether the current account has the required role
  • Whether the resource belongs to the current project or tenant
  • Whether the outbound IP is on the allowlist
  • Whether it is restricted by region, WAF, or security policies
  • Whether the response body explicitly indicates permissions or rate limiting
  • Whether rate-limit information exists in the response headers

Receiving 429

  • Whether Retry-After was read
  • Whether rate-limit-related response headers were checked
  • Whether exponential backoff and random jitter are used
  • Whether a maximum number of retries is set
  • Whether concurrency is limited
  • Whether duplicate query results are cached
  • Whether requests can be merged or sent in batches
  • Whether the plan quota or account usage limit has been reached

It is recommended to record the request time, API hostname and path, HTTP method, status code, business error code, request-id or trace-id, Retry-After, remaining rate-limit quota and reset time, number of retries, and actual wait time in logs.

However, do not record the complete API Key, complete Access Token, Refresh Token, user password, or sensitive personal information in the request body.

Final rule of thumb

401: Fix the credentials first; do not retry with the old Token.
403: Fix the permissions or policies first; do not repeat the request as is.
429: Respect the wait time; back off, slow down, and limit concurrency.

Status codes are only the entry point for troubleshooting, not the entire answer. A reliable approach is to combine HTTP status codes, business error codes in the response body, relevant response headers, platform documentation, request IDs, and server-side logs, turning “API errors” into an observable, recoverable, and maintainable engineering mechanism.

References