Grenzen von Baukasten-Formularen7 Min. Lesezeit
Netlify-Forms-Limit: was bei 100 Einsendungen passiert und wie du rauskommst
Legacy-Tarife von Netlify messen Formulareinsendungen pro Site und stufen dich automatisch hoch, und Credit-Tarife können die Site pausieren. Verlege das Formular deshalb auf ein Backend.
In den Legacy-Tarifen von Netlify enthält jede Site 100 Formulareinsendungen pro Monat, und wer darüber kommt, wird automatisch auf eine bezahlte Forms-Stufe hochgestuft. In den Credit-Tarifen für Accounts, die seit September 2025 erstellt wurden, sind Formulare kostenlos, aber bei einem kostenlosen Team, dem die Credits ausgehen, wird jede Site pausiert, Formulare eingeschlossen. Das Formular zu Ikiform zu verlegen heißt, die Netlify-Attribute zu entfernen und die action zu setzen, in reinem HTML oder jedem Framework.
Was bei Netlify anders ist
Netlify ist kein Site-Builder, sondern ein Host mit eingebauten Formularen. Das prägt sowohl das Limit als auch den Ausweg. Welches Limit gilt, hängt davon ab, wann dein Account erstellt wurde:
| Account | Was die Doku von Netlify sagt | Was es für ein viel genutztes Formular bedeutet |
|---|---|---|
| Legacy-Tarife, erstellt vor dem 4. September 2025 | Forms Level 0 enthält 100 Einsendungen und 10 MB Uploads pro Site und Monat. Wer darüber kommt, wird auf Forms Level 1 mit 1.000 Einsendungen hochgestuft, abgerechnet am Ende des Abrechnungszeitraums. Darüber hinaus fügen Zusatzpakete je 500 hinzu | Eine Spamwelle kann eine Site auf eine bezahlte Stufe heben, ohne dass jemand das entschieden hat. In der kostenlosen Legacy-Stufe pausiert das Erreichen eines Limits jede Site des Accounts |
| Credit-Tarife, seitdem erstellt | Formulareinsendungen sind kostenlos und unbegrenzt, ohne Stufen | Das Formular selbst kostet nichts. Aber wenn ein kostenloses Team seine monatlichen Credits verbraucht, pausiert Netlify alle seine Projekte, und pausierte Projekte empfangen keine Formulareinsendungen |
Die Doku von Netlify sagt klar, dass die Legacy-Hochstufung automatisch erfolgt und dass du nicht zurückstufen kannst, solange die Nutzung über der niedrigeren Stufe liegt. Prüf den Tariftyp deines Teams und die Formularnutzung der Site im Netlify-Dashboard, bevor du etwas entscheidest.
Der zweite Grund für den Wechsel hat nichts mit Zahlen zu tun. Netlify Forms funktioniert, indem es dein deploytes HTML liest:
- Formulare werden beim Deploy gefunden. Ein Formular, das React, Vue oder Svelte rendert, existiert in diesem HTML nicht, also hältst du eine versteckte statische Kopie für den Build bereit und sendest mit einem
form-name-Feld daran. - Bei Next.js braucht der OpenNext-Adapter diese Kopie als statische Datei in
public/, und der Post muss an diese statische Datei gehen, nicht an eine Next.js-Seite. Der Build scheitert, wenndata-netlifyohne sie in React-Code vorkommt. - Einsendungen müssen urlencoded oder multipart sein. JSON wird nicht akzeptiert.
- Das Formular funktioniert nur, solange die Site auf Netlify deployt ist. Den Host wechseln heißt, die Formulare umzuziehen.
- In einem Legacy-Tarif startet eine Site den Monat auf Forms Level 0 mit 100 enthaltenen Einsendungen.
- Einsendung 101 kommt an. Die Nutzungsdoku von Netlify beschreibt die Nutzung der Site, also kann ein Spamschub genau das sein, was sie über die Grenze bringt.
- Netlify stuft die Site auf Forms Level 1 mit 1.000 Einsendungen hoch und rechnet am Ende des Zeitraums ab.
- Über Level 1 hinaus fügen Zusatzpakete je 500 Einsendungen hinzu.
- Sendet das Formular an Ikiform, liegt die Seite weiter auf Netlify, aber der Post geht an den Ikiform-Endpunkt.
- Netlify Forms empfängt den Post nie, also gibt es nichts zu zählen oder hochzustufen.
Das Formular umziehen, Stack für Stack
Bevor du Code änderst, erstelle ein Ikiform-Formular pro Netlify-Formularname und setze sein Honeypot field auf bot-field. Dann funktioniert dein bestehender versteckter Input weiter als Honeypot, ohne Umbenennung. Wähle deinen Stack:
Reines HTML auf Netlify braucht die wenigsten Änderungen. Lösche die Netlify-Attribute und den form-name-Input, richte action auf den Endpunkt und verschiebe den Pfad der Dankeseite als vollständige URL in _redirect. Der bot-field-Input bleibt dein Honeypot.
<form name="contact" method="POST" action="/thanks" data-netlify="true" netlify-honeypot="bot-field">
<input type="hidden" name="form-name" value="contact" />
<p hidden><input name="bot-field" /></p>
<label>Email <input type="email" name="email" required /></label>
<label>Topic
<select name="topic">
<option value="question">A question</option>
<option value="bug">Something is broken</option>
<option value="work">Working together</option>
</select>
</label>
<label>Message <textarea name="message" required></textarea></label>
<button type="submit">Send</button>
</form>Auf Netlify funktionierte ein Next.js-Formular mit Netlify Forms nur über eine statische Kopie in public/, etwa public/__forms.html, an die deine Komponente sendete. Lösche diese Datei, entferne jede Einstellung NETLIFY_NEXT_VERIFY_FORMS und sende aus der Komponente an Ikiform. Dank seiner action sendet das Formular auch ohne JavaScript.
"use client";
import { type FormEvent, useState } from "react";
const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
export function ContactForm() {
const [state, setState] = useState<"idle" | "sending" | "sent">("idle");
const [error, setError] = useState("");
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setState("sending");
setError("");
try {
const response = await fetch(ENDPOINT, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(event.currentTarget),
});
const result = await response.json();
if (result.ok) return setState("sent");
setError(result.error.fields?.message?.[0] ?? result.error.fields?.email?.[0] ?? result.error.message);
} catch {
setError("Couldn't send. Check your connection and try again.");
}
setState("idle");
}
if (state === "sent") return <p role="status">Thanks. We read every message and reply within a day.</p>;
return (
<form action={ENDPOINT} method="post" onSubmit={onSubmit}>
<input name="bot-field" type="text" tabIndex={-1} autoComplete="off" hidden />
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
<label htmlFor="topic">Topic</label>
<select id="topic" name="topic" defaultValue="question">
<option value="question">A question</option>
<option value="bug">Something is broken</option>
<option value="work">Working together</option>
</select>
<label htmlFor="message">Message</label>
<textarea id="message" name="message" required />
{error ? <p role="alert">{error}</p> : null}
<button disabled={state === "sending"}>{state === "sending" ? "Sending" : "Send"}</button>
</form>
);
}Eine statische Astro-Seite braucht überhaupt kein JavaScript. Entferne data-netlify aus dem Formular, setze den Endpunkt als action und baue die Weiterleitung aus site in astro.config.mjs. Die Seite kann vorgerendert bleiben.
---
const endpoint = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
const thanksUrl = new URL("/thanks", Astro.site).href;
---
<form method="post" action={endpoint}>
<input name="bot-field" type="text" tabindex="-1" autocomplete="off" hidden />
<label for="email">Email</label>
<input id="email" name="email" type="email" required />
<label for="topic">Topic</label>
<select id="topic" name="topic">
<option value="question">A question</option>
<option value="bug">Something is broken</option>
<option value="work">Working together</option>
</select>
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
<input type="hidden" name="_redirect" value={thanksUrl} />
<button>Send</button>
</form>Eine TanStack-Start-App auf Netlify rendert Formulare in React, was die Erkennung beim Deploy von Netlify nicht sehen konnte. Sende stattdessen über eine Server-Funktion und lass die versteckte statische Kopie weg. Setze IKIFORM_SECRET_KEY in den Umgebungsvariablen der Site in Netlify. Die Funktion reicht den Origin-Header des Browsers weiter, sodass Posts von localhost als kostenlose Tests zählen, bevor dein Workspace einen Tarif hat.
import { createFileRoute } from "@tanstack/react-router";
import { createServerFn, useServerFn } from "@tanstack/react-start";
import { getRequestHeader } from "@tanstack/react-start/server";
import { type FormEvent, useState } from "react";
const sendMessage = createServerFn({ method: "POST" })
.validator((data: unknown) => {
if (!(data instanceof FormData)) throw new Error("Expected form data");
return {
email: String(data.get("email") ?? "").trim(),
topic: String(data.get("topic") ?? "question"),
message: String(data.get("message") ?? "").trim(),
"bot-field": String(data.get("bot-field") ?? ""),
};
})
.handler(async ({ data }) => {
const origin = getRequestHeader("origin");
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
...(origin ? { Origin: origin } : {}),
},
body: JSON.stringify(data),
});
const result = await response.json();
return result.ok ? { sent: true, error: "" } : { sent: false, error: String(result.error.message) };
});
export const Route = createFileRoute("/contact")({ component: Contact });
function Contact() {
const send = useServerFn(sendMessage);
const [state, setState] = useState({ sent: false, error: "" });
const [pending, setPending] = useState(false);
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setPending(true);
try {
setState(await send({ data: new FormData(event.currentTarget) }));
} catch {
setState({ sent: false, error: "Couldn't send. Try again." });
} finally {
setPending(false);
}
}
if (state.sent) return <p role="status">Thanks. We read every message and reply within a day.</p>;
return (
<form method="post" onSubmit={onSubmit}>
<input name="bot-field" type="text" tabIndex={-1} autoComplete="off" hidden />
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
<label htmlFor="topic">Topic</label>
<select id="topic" name="topic" defaultValue="question">
<option value="question">A question</option>
<option value="bug">Something is broken</option>
<option value="work">Working together</option>
</select>
<label htmlFor="message">Message</label>
<textarea id="message" name="message" required />
{state.error ? <p role="alert">{state.error}</p> : null}
<button disabled={pending}>{pending ? "Sending" : "Send"}</button>
</form>
);
}Mit React Router im Framework-Modus leitet die action der Route den Post weiter. Die FormData geht unverändert durch, sodass bot-field Ikiforms Honeypot-Prüfung erreicht, und der Origin-Header des Browsers geht mit, sodass localhost-Posts als Tests zählen. Entferne die statische Kopie, die du für Netlify behalten hast.
import { data, Form, useNavigation } from "react-router";
import type { Route } from "./+types/contact";
export async function action({ request }: Route.ActionArgs) {
const form = await request.formData();
const origin = request.headers.get("origin");
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: {
Accept: "application/json",
Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
...(origin ? { Origin: origin } : {}),
},
body: form,
});
const result = await response.json();
if (result.ok) return { sent: true, error: "" };
return data({ sent: false, error: String(result.error.message) }, { status: response.status });
}
export default function Contact({ actionData }: Route.ComponentProps) {
const sending = useNavigation().state === "submitting";
if (actionData?.sent) return <p role="status">Thanks. We read every message and reply within a day.</p>;
return (
<Form method="post">
<input name="bot-field" type="text" tabIndex={-1} autoComplete="off" hidden />
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
<label htmlFor="topic">Topic</label>
<select id="topic" name="topic" defaultValue="question">
<option value="question">A question</option>
<option value="bug">Something is broken</option>
<option value="work">Working together</option>
</select>
<label htmlFor="message">Message</label>
<textarea id="message" name="message" required />
{actionData?.error ? <p role="alert">{actionData.error}</p> : null}
<button disabled={sending}>{sending ? "Sending" : "Send"}</button>
</Form>
);
}Eine SvelteKit-Seite auf Netlify kann direkt aus dem Browser senden, ohne eigene Form Action. Das action-Attribut hält das Formular vor der Hydration funktionsfähig. Lösche jede vorgerenderte Kopie des Formulars, die du für Netlify ergänzt hast.
<script lang="ts">
const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
let sent = $state(false);
let sending = $state(false);
let error = $state("");
async function send(event: SubmitEvent) {
event.preventDefault();
sending = true;
error = "";
try {
const response = await fetch(ENDPOINT, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(event.currentTarget as HTMLFormElement),
});
const result = await response.json();
if (result.ok) sent = true;
else error = result.error.message;
} catch {
error = "Couldn't send. Check your connection and try again.";
}
sending = false;
}
</script>
{#if sent}
<p role="status">Thanks. We read every message and reply within a day.</p>
{:else}
<form method="post" action={ENDPOINT} onsubmit={send}>
<input name="bot-field" type="text" tabindex="-1" autocomplete="off" hidden />
<label for="email">Email</label>
<input id="email" name="email" type="email" required />
<label for="topic">Topic</label>
<select id="topic" name="topic">
<option value="question">A question</option>
<option value="bug">Something is broken</option>
<option value="work">Working together</option>
</select>
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
{#if error}<p role="alert">{error}</p>{/if}
<button disabled={sending}>{sending ? "Sending" : "Send"}</button>
</form>
{/if}In Nuxt sendet die Komponente mit $fetch aus dem Browser, also gibt es keine Server-Route zu deployen. $fetch wirft bei einer Antwort außerhalb von 2xx, und Ikiforms Fehlerhülle liegt in data des Fehlers. Entferne das versteckte statische Formular aus public/ und das form-name-Feld.
<script setup lang="ts">
const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
const sent = ref(false);
const sending = ref(false);
const error = ref("");
async function send(event: Event) {
sending.value = true;
error.value = "";
try {
await $fetch(ENDPOINT, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(event.target as HTMLFormElement),
});
sent.value = true;
} catch (failure) {
const data = (failure as { data?: { error?: { message?: string } } }).data;
error.value = data?.error?.message ?? "Couldn't send. Check your connection and try again.";
} finally {
sending.value = false;
}
}
</script>
<template>
<p v-if="sent" role="status">Thanks. We read every message and reply within a day.</p>
<form v-else :action="ENDPOINT" method="post" @submit.prevent="send">
<input name="bot-field" type="text" tabindex="-1" autocomplete="off" hidden />
<label for="email">Email</label>
<input id="email" name="email" type="email" required />
<label for="topic">Topic</label>
<select id="topic" name="topic">
<option value="question">A question</option>
<option value="bug">Something is broken</option>
<option value="work">Working together</option>
</select>
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
<p v-if="error" role="alert">{{ error }}</p>
<button :disabled="sending">{{ sending ? "Sending" : "Send" }}</button>
</form>
</template>Eine React-Single-Page-App auf Netlify sendete urlencoded Daten mit einem form-name-Feld an / und brauchte für die Erkennung eine statische Kopie in index.html. Jetzt sendet sie die FormData an den Endpunkt. Nur der Request ändert sich:
const body = new URLSearchParams(new FormData(event.currentTarget) as unknown as Record<string, string>);
body.set("form-name", "contact");
await fetch("/", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: body.toString(),
});Lösche das versteckte <form netlify> aus index.html, sobald nichts mehr an Netlify sendet.
Derselbe Post aus dem Browser in einer Vue-Single-File-Component. Entferne das versteckte Formular aus index.html, mit dem Netlify es erkannt hat.
<script setup lang="ts">
import { ref } from "vue";
const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
const state = ref<"idle" | "sending" | "sent">("idle");
const error = ref("");
async function send(event: Event) {
state.value = "sending";
error.value = "";
try {
const response = await fetch(ENDPOINT, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(event.target as HTMLFormElement),
});
const result = await response.json();
if (result.ok) {
state.value = "sent";
return;
}
error.value = result.error.message;
} catch {
error.value = "Couldn't send. Check your connection and try again.";
}
state.value = "idle";
}
</script>
<template>
<p v-if="state === 'sent'" role="status">Thanks. We read every message and reply within a day.</p>
<form v-else :action="ENDPOINT" method="post" @submit.prevent="send">
<input name="bot-field" type="text" tabindex="-1" autocomplete="off" hidden />
<label for="email">Email</label>
<input id="email" name="email" type="email" required />
<label for="topic">Topic</label>
<select id="topic" name="topic">
<option value="question">A question</option>
<option value="bug">Something is broken</option>
<option value="work">Working together</option>
</select>
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
<p v-if="error" role="alert">{{ error }}</p>
<button :disabled="state === 'sending'">{{ state === "sending" ? "Sending" : "Send" }}</button>
</form>
</template>IKIFORM_SECRET_KEY in Umgebungsvariablen, die nur der Server sieht.Jede Version behält dieselben Feldnamen wie bei Netlify, sodass Benachrichtigungs-E-Mails und CSV-Spalten vertraut aussehen. Der Vergleich mit Netlify Forms enthält die vollständige Zuordnung der Attribute, einschließlich data-netlify-recaptcha und Datei-Inputs.
Deploy Previews und erlaubte Domains
Netlify gibt jedem Pull Request eine eigene Adresse, etwa deploy-preview-42--your-site.netlify.app. Das ist keine Subdomain von your-site.netlify.app, also deckt die Adresse deiner Site unter Allowed domains keine Previews ab, und Posts aus Previews landen mit origin_mismatch im Spam.
- Füge
your-site.netlify.appund deine eigene Domain für die Produktion hinzu. - Füge einen bestimmten Preview-Host hinzu, solange du ihn testest, und entferne ihn dann.
- Füge nicht
netlify.appallein hinzu. Jede Site auf Netlify dürfte dann an dein Formular senden. - Oder richte Previews über eine Umgebungsvariable auf ein zweites Ikiform-Formular ohne erlaubte Domains und halte die Produktion streng.
Umstellen, ohne Einsendungen zu verlieren
- Lade die bisherigen Einsendungen jedes Formulars als CSV im Bereich Forms der Site herunter. Ikiform hat keinen Import.
- Deploye das neue Formular in eine Preview und sende einen Test. Prüfe die Feldnamen im Ikiform-Dashboard.
- Merge in die Produktion und sende einen weiteren Test von der Live-Domain.
- Ersetze jeden ausgehenden Netlify-Webhook durch einen Ikiform-Webhook. Netlify signiert mit einem JWS-Token; Ikiform nutzt Header nach Standard Webhooks, also ändert sich auch dein Verifizierer.
- Wenn ein paar Tage lang kein Post mehr Netlify erreicht hat, schalte die Formularerkennung in den Forms-Einstellungen der Site aus.
422 files_not_supported. Braucht ein Formular Dateien, lass dieses eine Formular bei Netlify Forms und zieh den Rest um.Prüfen, dass es auf Netlify funktioniert
- Füge unter Allowed domains des Formulars die Domain deiner Site hinzu, plus
localhost, während du entwickelst. Lass das Feld leer, um Posts von überall anzunehmen. - Deploye eine Preview, füge ihren Host hinzu und sende das Formular von dort ab.
- Öffne Submissions im Dashboard. Die neue Einsendung ist da, mit jedem Feld, das du gesendet hast.
- Prüfe, dass die Felder
email,topicundmessagesind, ohneform-nameund ohnebot-field. - Öffne den Tab Forms der Site in Netlify und prüfe, dass der Test nicht dort ist.
- Sende einmal mit ausgefülltem
bot-field. Er wird als Spam gespeichert und sendet nichts. - Prüfe das Postfach jedes Benachrichtigungsempfängers. Jeder bekommt eine E-Mail mit den Feldern, und Reply geht an den Absender, wenn das Formular ein
email-Feld hat.
Stolperfallen
| Problem | Was zu tun ist |
|---|---|
Ein form-name-Feld taucht in Ikiform auf | Der versteckte form-name-Input ist noch im Formular, oder dein Code setzt ihn noch. Entferne ihn; Ikiform erkennt das Formular an der URL. |
| Der Next.js-Build scheitert mit einer Meldung zu Formularen | Ein Attribut netlify oder data-netlify steht noch in deinem React-Code. Entferne es zusammen mit public/__forms.html. |
| Previews senden in den Spam | Der Preview-Host steht nicht unter Allowed domains. Füge ihn beim Testen hinzu oder nutze ein zweites Formular für Previews. |
| Jeder Post wird als Spam markiert, auch echte | Honeypot field ist noch _gotcha, während dein Input bot-field heißt, oder umgekehrt. Bring sie in Einklang. |
422 files_not_supported | Das Formular hat ein <input type="file"> mit ausgewählter Datei. Datei-Uploads werden noch nicht unterstützt; entferne das Eingabefeld oder bitte stattdessen um einen Link. |
429 rate_limited | Ein Besucher hat mehr gesendet, als das Rate-Limit des Formulars erlaubt. Standard erlaubt 5 pro Minute und Besucher. Warte Retry-After ab oder wähle Relaxed oder Custom für Formulare, die von einer gemeinsamen IP aus senden. |
Fragen
In Legacy-Tarifen stuft Netlify die Site auf Forms Level 1 hoch, das 1.000 Einsendungen pro Monat enthält, und rechnet am Ende des Abrechnungszeitraums ab. In der kostenlosen Legacy-Stufe pausiert das Erreichen eines Limits jede Site des Accounts, bis du upgradest.
Laut Doku von Netlify sind Formulareinsendungen in Credit-Tarifen kostenlos und unbegrenzt. Der Haken ist die Site selbst: Wenn einem kostenlosen Team die monatlichen Credits ausgehen, werden seine Projekte pausiert und empfangen keine Formulareinsendungen.
Ja. Ikiform ist egal, wo die Seite gehostet ist. Entferne die Netlify-Attribute aus dem Formular, setze seine Action auf deinen Ikiform-Endpunkt, und Netlify Forms sieht es nicht mehr.
Nein. Diese Kopie gibt es nur, damit der Deploy-Schritt von Netlify das Formular findet. Ikiform nimmt Posts an einer URL an, also lösche die Kopie und sende aus deiner Komponente.
Nein, es gibt keinen kostenlosen Tarif. Testen ist kostenlos: Du kannst 50 Testeinsendungen pro Monat von localhost senden, bevor du zahlst. Für den Livebetrieb startet Pro bei $6/Monat für 500 Einsendungen, oder du zahlst einmalig 10.000 Einsendungen für $49 oder 50.000 Einsendungen für $149, mit Guthaben, das nie verfällt.
Hängst du bei einem Schritt fest oder willst eine Anleitung für deinen Fall? Schreib an hello@ikiform.com.