Is serving a full shop-floor UI directly from n8n webhooks a sane production architecture? (looking for honest, critical feedback)

First, some context: I’m not an IT professional. I work on the shop floor and taught myself all of this in my spare time, out of interest — so please assume no formal background, and don’t hold back if something is naive.
I’ve built a small MES (manufacturing execution system) for a factory and I’d like an honest reality-check from people who use n8n seriously, because I may be pushing it well past its intended use.
The setup. A small factory assembles machines on production lines. Each unit moves through 5 departments across 6 lines. There are around 30 shop-floor terminals (kiosk browsers), one per workstation.
The architecture. n8n is the center of everything. Every page the operators see is HTML generated inside n8n Code nodes and returned via Respond to Webhook (Content-Type: text/html). PostgreSQL holds the data. The pages are not just views: operators click buttons that call other n8n webhooks to advance a machine’s state (a state machine with ~8 actions), write notes, block/unblock units, etc. So n8n is serving the whole frontend, not only automation.
Access model. No login, by design. It’s a trusted shop floor; the permission travels in the URL (“the link is the permission”), and buttons only appear where the state and origin allow. Reasonable for the context, but unconventional.
Expected load. Modest. ~30 terminals, a handful of operators acting at any given moment. Actions happen on the scale of minutes, not high-frequency. A few thousand machines/year, ~40k state events/year. Overview pages would auto-refresh every 1–3 minutes.
My question. Is serving the entire interactive UI from n8n webhooks a defensible choice for production at this scale, or am I abusing the tool? Specifically:
What breaks first as this grows — executions history, performance, maintainability?
Would you split it (dedicated frontend + n8n as a backend API), and is that worth it if the current thing already works?
Is anyone actually running something like this in production, or is it a known anti-pattern?
I’d genuinely rather hear “don’t do this, and here’s why” than polite encouragement. Thanks.

Describe the problem/error/question

What is the error message (if any)?

Please share your workflow

(Select the nodes on your canvas and use the keyboard shortcuts CMD+C/CTRL+C and CMD+V/CTRL+V to copy and paste the workflow.)

Share the output returned by the last node

Information on your n8n setup

  • n8n version:
  • Database (default: SQLite):
  • n8n EXECUTIONS_PROCESS setting (default: own, main):
  • Running n8n via (Docker, npm, n8n cloud, desktop app):
  • Operating system:

Hi @Folpe77 Welcome!
Executions history breaks first, and it breaks quietly. Every page render and every auto-refresh is one saved webhook execution, so 30 terminals refreshing every 1 to 3 minutes lands somewhere around 15k to 40k executions a day against your ~40k state events a year. EXECUTIONS_DATA_PRUNE_MAX_COUNT defaults to 10000, so the log rolls over a few times a day and the state changes you would actually want to trace are gone within hours.
Split by role rather than splitting the frontend: page rendering in its own workflows with Save successful production executions set to Do not save, the state machine in separate workflows that keep saving. The render volume then stops mattering and the audit trail survives.

At this load, n8n can handle the traffic, but I would not keep the current trust model unchanged for production.

The main risks are not raw throughput; they are authorization, concurrency, and maintainability:

  • A URL is a bearer credential. It can leak through browser history, screenshots, logs, referrers, or bookmarks. At minimum use short-lived signed tokens tied to terminal + action, validate them server-side, and keep the network segmented. Never rely only on hiding buttons—the webhook must authorize every state transition.
  • Make each state change a transaction in PostgreSQL. Update only when the current state/version matches what the operator saw (optimistic locking), then return a conflict instead of silently overwriting a newer action.
  • Keep rendering workflows separate from command workflows, as Anshul suggested. Disable saved successful executions for reads, retain failed executions briefly, and preserve state-change events in a dedicated audit table rather than depending on n8n execution history.
  • Put the state-transition rules in one reusable sub-workflow or database function. Generating HTML across many Code nodes will become the first maintainability problem.
  • Add health checks, error workflows, database backups, and a small reconciliation job before expanding usage.

I would not rewrite a working low-load system immediately. I’d first harden authorization and transactional writes, isolate read vs. command workflows, and instrument latency/error rates. A separate frontend becomes worthwhile when UI iteration, offline behavior, richer authentication, or multiple developers make the embedded HTML painful—not simply because 30 kiosks are too much for n8n.

The Execution History (The “Silent Killer”) n8n is designed to log every single execution. In a standard automation, a workflow might run once an hour. In your setup, every page refresh and every button click is an execution.

  • The Risk: Your n8n database will bloat rapidly. Even with pruning enabled, the overhead of writing “Success” to the database for every single UI interaction will eventually slow down the entire system. You’ll notice the UI becoming “sluggish” not because the HTML is slow, but because the execution engine is struggling to log the event.

Maintainability (The “Code Node Nightmare”) Writing HTML inside a JavaScript Code node is a recipe for disaster.

  • The Risk: You have no syntax highlighting, no “live preview,” and no way to easily manage CSS/Styling. As you add the 9th or 10th state action, your Code nodes will become massive walls of text. A single missing </div> or a typo in a string will crash the entire page, and debugging it inside a small n8n window is agonizing.

Performance & Latency n8n is an orchestration engine. When a request hits a webhook, n8n has to initialize the workflow, move data through nodes, and then respond.

  • The Risk: This is orders of magnitude slower than a dedicated web server. For 30 terminals, it’s fine. If you ever scale to 100 terminals or add high-frequency updates, the “lag” between clicking a button and seeing the page refresh will become frustrating for the operators.

Yes. Absolutely. But you don’t have to become a full-stack developer to do it.

The “defensible” architecture would be:

  • Frontend: A dedicated UI that knows how to display data and send requests.
  • Backend: n8n acting as a JSON API​. Instead of returning HTML, your n8n workflows should return raw data (JSON).

Why is this worth it?

  1. Instant UI: The frontend can update a button color or a status label instantly without reloading the entire page from the server.
  2. Stability: If you want to change the layout of the page, you don’t have to touch your “business logic” in n8n.
  3. Execution Efficiency: You can set your n8n workflows to “Save execution: None” for simple API calls, which removes the database bloat entirely.

The practical way to decide this is to run a small “production drill” before rewriting anything.

Pick one state transition that matters, for example “block/unblock unit”, and verify three things:

  1. Two terminals clicking at nearly the same time cannot overwrite each other.
  2. A stale page cannot perform an action that is no longer valid.
  3. The event is still auditable later even if n8n execution history is pruned.

If those three hold, the current architecture is probably defensible at your scale while you harden it gradually. If they don’t, the first split should not be “new frontend”; it should be moving state transitions into a safer transactional layer and letting n8n orchestrate around it.

@Folpe77 your 1–3 minute auto-refresh on overview pages is itself a scaling risk independent of execution logging, polling doesn’t tell operators about a state change until the next refresh, so as you add lines you’ll be tempted to shorten the interval, which multiplies webhook load fast. A cheap fix that doesn’t require a full frontend rewrite: keep serving HTML from n8n, but swap polling for Server-Sent Events or a lightweight WebSocket relay (even a tiny separate Node process just for push) that n8n triggers on state change, terminals update instantly and you drop most of the “read” traffic hitting n8n at all.

@Anshul_Namdev s diagnosis is exactly right — split page-rendering from the state machine, keep the state machine saving. Worth being specific about what that gets you: once that history table is the real record of what happened on the floor, the next question is whether anything stops it from being quietly edited later — bad migration, direct DB access, a bug somewhere downstream. A plain table implies it wasn’t touched, doesn’t prove it.

Separately, for the approval side of your state machine — block/unblock, sign-offs, whatever needs a human in the loop: there’s a pattern where one webhook URL gets generated per workflow when it’s published, not per request. Every approval event for that workflow lands on the same URL and starts a fresh execution only when it actually arrives, so nothing sits open waiting and nothing’s at risk of getting pruned mid-wait the way Anshul described. Built a general version of this — not AI-agent-specific, works the same for a human-driven state machine like yours. Happy to share if useful