Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Explainer

Netlify Forms: What You Get Free, and Where the Ceiling Is

Sean

Platform Writer

Aug 20, 2026
8 min read

Add a data-netlify attribute to a form, deploy, and the platform parses your HTML at build time and creates an endpoint for it. Submissions land in a dashboard inbox. There is no server, no route handler, and no database — which is the appeal and also the constraint.

Netlify Forms: What You Get Free, and Where the Ceiling Is

The static-site problem has always been the contact form. Everything else about a static site is better than the alternative: fast, cheap, hard to break, trivial to cache. Then somebody wants to be emailed when a visitor fills something in, and suddenly you need a server.

Forms as a platform feature is a neat answer to that. It is worth understanding what it does at build time, because that is where its behaviour and its surprises both come from.

Table of contents

How it works, and why that matters

At deploy time, the build system parses your generated HTML looking for forms marked with a specific attribute. Each one it finds gets an endpoint provisioned for it, keyed on the form’s name.

<form name="contact" method="POST" data-netlify="true">
  <input type="hidden" name="form-name" value="contact" />
  <p>
    <label>Name <input type="text" name="name" required /></label>
  </p>
  <p>
    <label>Email <input type="email" name="email" required /></label>
  </p>
  <p>
    <label>Message <textarea name="message" required></textarea></label>
  </p>
  <p><button type="submit">Send</button></p>
</form>

The name attribute is what the form is called in the dashboard, and the hidden form-name field is how a submission identifies which form it belongs to. Both are required. Omitting the hidden field is the most common reason a form deploys successfully and then returns a 404 on submit.

The build-time parsing is the crucial detail, and it is the source of nearly every problem people hit. The form must exist in the HTML that gets deployed. If your form is rendered by JavaScript after the page loads — a React or Vue component, a modal that mounts on click — the build never sees it, so no endpoint is created.

The workaround is to ship a plain HTML version for the parser to find, then let your framework render the real one. It is a hidden static form whose only job is to be seen at build time, which is exactly as inelegant as it sounds and works reliably.

Spam handling without a visible challenge

Every public form endpoint gets spam within days. There are two built-in defences, and the first one is free and invisible.

A honeypot is a field that real users never fill in because they never see it. Bots fill in every field they find:

<form name="contact" method="POST" data-netlify="true" netlify-honeypot="bot-field">
  <p class="hidden">
    <label>Do not fill this in: <input name="bot-field" /></label>
  </p>
  <!-- real fields follow -->
</form>

Hide it with CSS rather than type="hidden", since a genuinely hidden input is a hint to bots too. Submissions with that field populated get filed as spam automatically.

The second defence is a visible challenge, which works better and costs you conversions. The honest trade-off: a honeypot alone stops most automated spam and inconveniences nobody. Add the visible challenge only when the honeypot stops keeping up, not preemptively.

Whatever you use, notifications matter more than filtering. A submissions inbox nobody opens is a form that does not work, and this failure mode is silent — the form submits successfully, the user gets a thank-you page, and the message sits unread for three weeks. Wire up an email or webhook notification the same day you add the form.

Chaining a function onto a submission

The default flow ends at the inbox. Most real uses want something to happen — a notification in a chat channel, a record in a CRM, an autoresponder, a row in a database.

A submission event can trigger a serverless function, which is where the pattern gets genuinely useful:

// netlify/functions/submission-created.js
export default async (req) => {
  const { payload } = await req.json();
  const { name, email, message } = payload.data;

  await fetch(process.env.CRM_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name, email, message, source: 'website-contact' })
  });

  return new Response('ok');
};

The function name is the trigger — a function named for the submission event runs on every submission, with no wiring required. The API key lives in an environment variable rather than in the bundle, which is the whole point of doing it server-side.

This is the shape at which the feature is at its best: a static site, a form endpoint you did not have to build, and one small function that forwards the data wherever it needs to go.

The ceiling, and how you hit it

Included submission volume is modest, and the step up to a paid tier is priced per site. For a contact form on a marketing site that is comfortable and probably permanent. For anything that submits at real volume, the arithmetic changes quickly.

The functional limits arrive sooner than the volume ones:

  • No querying. The inbox is a list. You cannot ask which submissions came from a campaign, or how many arrived last week, or join them to anything.
  • No server-side validation before storage. Validation is client-side, which means it is advisory. Whatever a bot posts to the endpoint gets stored.
  • No editing or state. Submissions are immutable records. There is no way to mark one handled, assign it, or track it through a process.
  • File uploads count against storage and have size limits that a document-heavy form will find.
  • It is not a database. This is the real one. Anything requiring relationships, updates or queries has outgrown the feature by definition.

The recognisable sequence: a contact form, then a job application form, then someone wants to filter applications by role, then someone wants to mark them reviewed. Nothing on that path is unreasonable, and by the end you are asking a submissions inbox to be an applicant tracking system.

There is also portability to consider. Your submissions live in one platform’s dashboard, and moving them elsewhere later is an export-and-reimport job with whatever fidelity the export gives you.

When to write the endpoint yourself

Once the form is doing real work, a small API route with a database behind it is not much more effort and removes every limit above at once.

// A minimal handler, on any platform that runs a process
app.post('/api/contact', async (req, res) => {
  const { name, email, message } = req.body;

  if (!name || !email || !message) {
    return res.status(400).json({ error: 'Missing required fields' });
  }
  if (message.length > 5000) {
    return res.status(400).json({ error: 'Message too long' });
  }

  await db.query(
    'INSERT INTO contact_submissions (name, email, message, created_at) VALUES (?, ?, ?, NOW())',
    [name, email, message]
  );

  res.json({ ok: true });
});

The validation runs where it cannot be bypassed. The data is in a table you can query, index, join and back up. Marking something handled is an UPDATE. Exporting is a SELECT. And the data is yours in a format any tool can read.

The cost is that you now need somewhere for that process to run and a database for it to write to. Which is a real cost, and a smaller one than the alternative once the form matters.

How this fits the rest of the stack

A form is usually the first thing on a static site that needs to remember something, and it is the moment the site quietly becomes an application. Platform form features are a good bridge across that gap. They are not a place to keep data you intend to use.

RunxBuild is built for the far side of that bridge. A static site builds from your GitHub repository with custom domains, headers and redirects, and 120GB of bandwidth included at $0.10/GB after. When the form needs a real endpoint, a web service in Node, Python, Go, Ruby, Java, .NET or Docker deploys from the same repository with environment variables, runtime logs and rollback, and a managed MySQL or Postgres sits beside it with backups, connection limits and private networking — so the submissions land in a table you can query rather than a list you can scroll. To see what the site, the service and the database add up to, the RunxBuild hosting calculator lists them as separate line items.

Useful related references:

FAQ

Why is my Netlify form returning a 404 when I submit it?

Almost always one of two things: the hidden form-name field is missing, or the form is rendered by JavaScript so the build-time HTML parser never saw it. Endpoints are provisioned at build time from the deployed HTML — if the form only exists after hydration, no endpoint was created. Ship a plain static copy for the parser to find.

How do I stop spam without adding a visible challenge?

Use the honeypot attribute with a field hidden by CSS rather than by type="hidden". Real users never fill it in; bots fill in everything. Submissions with it populated are filed as spam automatically. Add a visible challenge only if the honeypot stops keeping up — it works better and costs you conversions.

Can I run code when a form is submitted?

Yes. A serverless function named for the submission-created event fires on every submission and receives the payload, so you can forward the data to a CRM, a chat channel or an email service. The credentials live in environment variables rather than in the client bundle, which is the main reason to do it server-side.

Are form submissions a substitute for a database?

No, and this is the limit that arrives first. The inbox is an append-only list: you cannot query it, filter it, join it to anything, or mark a submission as handled. Client-side validation is advisory, so whatever gets posted to the endpoint is stored. Anything that needs relationships or updates has outgrown it.

How many submissions do I get for free?

The included allowance is modest — suited to a contact form on a marketing site rather than to anything high-volume — and the paid step up is priced per site. Check the current numbers before building something that submits at scale, because the functional limits usually bite before the volume limits do.

#netlify forms#netlify#static site#forms#jamstack