InstaWebhook
August 15, 2026By InstaWebhook TeamWebhook Security

Handling Webhook Payloads That Exceed Server Limits (Stripe, GitHub & Beyond)

Handling Webhook Payloads That Exceed Server Limits (Stripe, GitHub & Beyond) Webhooks are the backbone of event-driven architecture.

413 payload too large413 payload too large errorapi gateway webhook payload limitasync webhook handlingbig json webhook storagebody parser payload too large webhookcloudflare webhook payload limitexpress body parser limit webhookfastapi request size limit webhookfast webhook handling large datagithub commit payload too largegithub push event payload too largegithub webhook large diffgithub webhook size limithandling big webhookshandling large http post requesthandling large json webhooksincrease webhook payload size limitingress payload size limitInstaWebhook json storageInstaWebhook large payloadkubernetes ingress body size webhooklarge webhook payloadmassive webhook payload handlingmax webhook payload sizenginx 413 payload too largenginx client max body size webhooknodejs webhook payload limitprocessing large webhook payloadss3 webhook payload storagesecure webhook storage large payloadsstore large webhook jsonstripe event body size limitstripe webhook payload limitstripe webhook payload too largestripe webhook size errorwebhook architecture patternswebhook architecture scalabilitywebhook body buffer limitwebhook body parsing error 413webhook body size http 413webhook body size limitwebhook data ingestion limitswebhook ingress configurationwebhook listener size limitswebhook max body size configurationwebhook payload compressionwebhook payload offloadingwebhook payload optimizationwebhook payload thresholdwebhook payload truncationwebhook request body limitwebhooks 413 error fixwebhook size limitwebhook streaming payload
Handling Webhook Payloads That Exceed Server Limits Stripe Git Hub Beyond

Handling Webhook Payloads That Exceed Server Limits (Stripe, GitHub & Beyond)

Webhooks are the backbone of event-driven architecture. They let platforms like GitHub, Stripe, Shopify, and Slack push real-time updates straight into your backend.

But as an integration scales, developers eventually hit the same wall: HTTP 413 Payload Too Large.

A big GitHub push touching thousands of files, or a heavy Stripe batch event, can balloon a request from a few kilobytes into tens of megabytes. When that hits a server or serverless function configured with default limits, the ingress layer drops the request before your application code ever sees it.

This guide covers why webhook payloads get rejected, how to raise the relevant limits (Nginx, Express, API Gateway), the architectural patterns that solve the problem properly, and where a managed webhook-delivery service fits in.

The Anatomy of a Large Webhook Payload

Most webhooks are compact — a typical charge.succeeded or issue_comment.created event is well under 20 KB. Payloads grow large in a few predictable situations:

GitHub push events and repository syncs. A push containing many commits, large diffs, or submodule updates can produce a sizeable JSON body. GitHub caps webhook payloads at 25 MB; if an event would generate something larger, GitHub simply does not deliver it at all rather than sending a truncated payload.

Stripe batch and subscription events. Deeply nested invoice line items or account-migration events can produce heavy JSON. The bigger operational risk with Stripe, though, isn't payload size — it's response time. Stripe expects your endpoint to return a 2xx quickly, and if processing takes too long, the delivery is marked failed and retried with exponential backoff for up to three days in live mode before Stripe gives up on that event.

Why Default Server Configurations Reject Large Bodies

Web servers and frameworks cap request body size by default, mainly as a guard against denial-of-service abuse. Here's where those defaults actually sit today:

Infrastructure LayerDefault Payload LimitBehavior on Limit Exceeded
Express.js (express.json())100 KBReturns 413 Payload Too Large
Nginx (client_max_body_size)1 MBReturns 413 Request Entity Too Large
AWS API Gateway (REST / HTTP)10 MB, hard cap for buffered requestsReturns 413 Payload Too Large
AWS Lambda (synchronous invoke)6 MBReturns 413 or invocation error
AWS Lambda (asynchronous invoke)1 MB (raised from 256 KB in Oct 2025)Rejected before invocation
Cloudflare Workers100 MB (Free/Pro) · 200 MB (Business) · 500 MB (Enterprise default)Returns 413 Request Entity Too Large
Apache (LimitRequestBody)0 (unlimited) by directive default, though most distro configs cap it well below 1 GBReturns 413 Request Entity Too Large

A few things worth calling out, since this table shifts more often than people expect:

  • AWS Lambda's async limit doubled recently. Asynchronous invocations (SNS, EventBridge, S3 notifications, direct async Invoke calls) moved from 256 KB to 1 MB in October 2025, specifically to reduce the need for chunking or offloading LLM-style payloads and telemetry data. Synchronous invocations — the path most webhook receivers built on Lambda + API Gateway actually use — are still capped at 6 MB.
  • API Gateway's 10 MB ceiling is no longer absolute for responses. As of November 2025, API Gateway REST APIs support response streaming for backends that support it (Lambda, HTTP proxy, private integrations), which removes the 10 MB ceiling on the response side and extends integration timeouts to 15 minutes. The 10 MB limit on incoming request payloads is unchanged — this update helps you stream large data back out, not accept larger webhooks in.
  • Cloudflare's request body limit is tied to your zone's plan, not a fixed number across all users. It's meaningfully higher than the 100 KB–50 MB range often quoted online — even the free tier allows up to 100 MB.

Step 1: Reconfiguring Server Ingress for Large Webhooks

If you control the server or reverse proxy, raising the body-size ceiling at the ingress layer is the first move.

Nginx

Nginx enforces a default client_max_body_size of 1 MB. A 5 MB GitHub push event gets rejected before it ever reaches your application process.

Code example
# /etc/nginx/sites-available/webhook-service.conf

server {
    listen 443 ssl http2;
    server_name webhooks.yourdomain.com;

    # Expand body limit specifically for webhook endpoints
    client_max_body_size 50M;

    location /api/v1/webhooks/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Ensure long-running payloads don't drop the proxy connection
        proxy_read_timeout 60s;
        proxy_connect_timeout 60s;
    }
}

Test and reload: sudo nginx -t && sudo systemctl reload nginx.

Node.js and Express

express.json() defaults to a 100 KB limit. You'll want to raise it — but the more important detail is preserving the raw, unparsed body buffer, since HMAC signature verification for GitHub and Stripe depends on hashing the exact bytes that were sent, not a re-serialized version of the parsed object.

Code example
import express, { Request, Response } from 'express';

const app = express();

interface AuthenticatedRequest extends Request {
  rawBody?: Buffer;
}

// Support larger JSON payloads while capturing raw bytes for HMAC verification
app.use(
  express.json({
    limit: '50mb',
    verify: (req: AuthenticatedRequest, res: Response, buf: Buffer) => {
      req.rawBody = buf;
    },
  })
);

app.post('/webhooks/github', (req: AuthenticatedRequest, res: Response) => {
  const signature = req.headers['x-hub-signature-256'];

  if (!verifyGitHubSignature(req.rawBody, signature)) {
    return res.status(401).send('Invalid signature');
  }

  // Acknowledge receipt immediately to avoid sender-side timeouts
  res.status(200).send({ received: true });

  // Process the payload asynchronously from here
});

app.listen(3000, () => console.log('Webhook receiver running on port 3000'));

Note: if you're handling Stripe webhooks specifically, Stripe's official guidance is to use express.raw() (not express.json()) on that route, since the SDK's signature-verification helper needs the raw body directly — running a JSON body-parser globally ahead of it will break verification.

Python (FastAPI)

In FastAPI or Flask behind Gunicorn/Uvicorn, check Content-Length before reading the full body into memory, and offload processing to a background task so you're not holding the connection open while you work.

Code example
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks

app = FastAPI()

MAX_PAYLOAD_SIZE = 50 * 1024 * 1024  # 50 MB

@app.post("/webhooks/stripe")
async def handle_stripe_webhook(request: Request, background_tasks: BackgroundTasks):
    content_length = request.headers.get("content-length")
    if content_length and int(content_length) > MAX_PAYLOAD_SIZE:
        raise HTTPException(status_code=413, detail="Payload exceeds 50MB limit")

    body = await request.body()
    if len(body) > MAX_PAYLOAD_SIZE:
        raise HTTPException(status_code=413, detail="Payload exceeds 50MB limit")

    background_tasks.add_task(process_heavy_payload, body)

    return {"status": "accepted"}

Architectural Patterns for Handling Big Webhooks

Bumping ingress limits is a short-term patch. Parsing a 25 MB JSON blob synchronously inside your main application process creates real problems:

  • CPU/event-loop starvation — parsing that much JSON blocks Node's single-threaded event loop or burns CPU cycles in Python.
  • Provider timeouts — Stripe and most other providers expect a fast 2xx; slow processing gets marked as a failed delivery and retried, which can multiply your load during an incident.
  • Hard cloud ceilings — API Gateway's 10 MB request limit is not configurable. You can't raise it, full stop; you have to route around it.

Two patterns solve this properly.

Pattern 1: The Claim-Check Pattern

Instead of pushing the full payload through your event bus or API layer, store it in object storage (S3, GCS) and pass a lightweight reference through your pipeline instead.

  1. A lightweight edge receiver accepts the large payload.
  2. It writes the full, unparsed JSON to S3/blob storage.
  3. It publishes a small reference event — { "event_id": "evt_123", "s3_key": "webhooks/2026/08/15/evt_123.json" } — to SQS, RabbitMQ, or Kafka.
  4. It immediately responds 200 OK to the sender.
  5. A worker fetches the full JSON from storage, processes it, and archives or deletes it per your retention policy.

Pattern 2: Thin Payloads and API Polling

Some providers support sending a minimal notification containing just an event type and a resource ID, rather than the full object:

Code example
{
  "id": "evt_3MvL2e2eZvKYlo2C",
  "type": "invoice.payment_succeeded",
  "data": {
    "object": {
      "id": "in_1MvL2e2eZvKYlo2C"
    }
  }
}

Your handler validates the signature, enqueues the resource ID, returns 200 OK immediately, and a background worker calls the provider's REST API to pull the full object on demand.

  • Advantage: keeps webhook payloads under a couple of KB, sidestepping size limits entirely.
  • Trade-off: more outbound API calls, which means watching provider rate limits (Stripe, for example, allows 100 requests/second in live mode).

Where a Managed Webhook Layer Fits

Custom Nginx tuning, S3 buckets for the claim-check pattern, memory-safe streaming, and dead-letter queues add real engineering overhead — enough that a number of teams choose to put a managed layer in front of their own infrastructure instead of building all of this themselves.

This is a genuine product category, not just a hypothetical: Svix (which also maintains the open Standard Webhooks spec) and Hookdeck are established, independently verifiable options that handle intake, retries, and signature verification as a hosted service. Newer, smaller entrants like InstaWebhook offer a similar shape of product — durable intake, encrypted payload storage, retry/replay with visible delivery states, and either hosted or "bring your own database" storage modes.

Whichever you evaluate, the pitch is the same: a buffer sits between the provider and your backend, accepts the payload immediately (so you never return a 413 or a timeout to GitHub or Stripe), stores it durably, and hands your application a smaller, already-validated payload — or lets you pull it on your own schedule.

Worth being clear-eyed about: this shifts where the payload-size problem is handled, not whether one exists — you're trading in-house infrastructure work for a vendor dependency and, in hosted mode, having your payloads pass through a third party's storage. For sensitive data, check whether a given provider offers a self-hosted or BYO-database storage mode before committing.

Technical Checklist for Handling Webhook Payloads

  • Ingress limit verification. Confirm Nginx, HAProxy, or Cloudflare rules allow body sizes matching the largest payload you actually expect (25 MB+ for GitHub headroom, for example).
  • Preserve raw bytes. Don't run JSON.parse() or a body-parser before validating the HMAC signature — re-serialization changes whitespace and key order, which breaks signature validation.
  • Respond fast, process async. Return 200/202 within a couple of seconds of receipt; hand parsing and database writes to a background worker (Celery, BullMQ, Sidekiq).
  • Idempotency. Log delivery IDs (X-GitHub-Delivery, Stripe's evt_...) with a TTL so retried deliveries don't get processed twice.
  • Claim-check for anything near a hard cloud ceiling. If you're on API Gateway (10 MB) or a similarly fixed limit, route large payloads through S3 rather than trying to raise a limit that can't be raised.

Conclusion

A 413 error is close to a rite of passage for a growing integration surface. Raising ingress limits in Nginx or Express buys you immediate relief, but durable stability comes from decoupling ingestion from processing — via the claim-check pattern, thin payloads plus polling, or a managed intake layer that takes the buffering problem off your plate entirely.


Sources