Most AI chat tutorials end the same way: call the model, stream the tokens, done. What happens when someone pastes "ignore your previous instructions" into the box? What happens when a script hits your endpoint in a loop a thousand times in a minute? What happens when the model, mid-response, starts writing Python for a user who asked an unrelated question, because nothing told it not to?
None of these are model problems. They're engineering problems, and recently, while building a chat feature for my own portfolio site, I realized the guardrails around the model matter more than the model call itself. In this article, I'd like to share the five layers I actually run in front of a production chat feature, and what I learned building them.
The Problem With Trusting the Model
A guardrail is a plain, deterministic check that runs around a model call (before it, after it, or both) so the safety of the feature doesn't depend on the model behaving well. This matters more than it might seem: prompt injection is ranked LLM01, the #1 risk, in the OWASP Top 10 for LLM Applications, 2025 edition, ahead of every other AI-specific risk category.
Worth being precise here: prompt injection and jailbreaking overlap but aren't the same thing. Jailbreaking is getting a model to act against its own training or guidelines ("pretend you have no restrictions"). Prompt injection is broader: getting the model to follow attacker-supplied instructions instead of the developer's, which can arrive directly in a chat message or indirectly, buried in a document or webpage the model later processes. The five layers below mostly address direct injection and jailbreak-style phrasing arriving through the chat input itself.
A system prompt telling the model "don't reveal your instructions" is a suggestion the model can be talked out of. A regex that blocks a known pattern before it reaches the model gives you a deterministic enforcement point instead: it doesn't depend on the model choosing to cooperate. That's a real difference, but it's worth being exact about what it buys you: it stops that matched pattern, not every possible phrasing of the same intent. More on that in the "what this doesn't protect against" section below.
The Pipeline I Built
I built this for the chatbot on my own portfolio site, a FAQ-style assistant that answers questions about my background and projects, with a hard scope: it should talk about me, and decline everything else. That scope is exactly where guardrails matter, because "decline everything else" is precisely the instruction a jailbreak attempt tries to override.
Here's the shape of it end to end. The pipeline has seven stages; five of them are the guardrail layers this article is actually about, and two are the natural bookends, the request coming in and the response going out:
(opens full size in a new tab)
Stages 2–4 run before the model is ever called. Stage 6 runs while the model is still streaming its answer. Stage 5, the model call itself, is the only non-deterministic part, and it's where the fourth guardrail layer (provider safety settings) actually lives, as a configuration on that call rather than a separate stage of its own. Everything else in the pipeline is plain code with a testable contract.
Five Decisions, Five Layers
1. Request Validation. Before any model call, check the shape of what came in: is this actually an array of {role, content} pairs, is each message under a sane length, is the history capped to a reasonable window.
if (!Array.isArray(messages) || messages.length === 0 || messages.length > MAX_TOTAL_MESSAGES) {
return { error: 'Invalid request' };
}
Only the recent window of history needs to reach the model. A FAQ-style assistant with a static system prompt doesn't need the full conversation to answer a follow-up; it needs the last few exchanges.
2. Per-IP Rate Limiting. A sliding-window counter, in memory, per server instance.
const recent = (hits.get(ip) ?? []).filter(t => now - t < WINDOW_MS);
const allowed = recent.length < MAX_REQUESTS;
This stops a script looping against your endpoint or a curious user mashing the button. It does not stop a real distributed attack spread across thousands of IPs; know which one you're building before you rely on it.
3. Prompt-Injection Prefilter. The layer most tutorials skip, and the cheapest one to add: a small set of regex patterns catches obvious direct-injection and jailbreak-style phrasing before the message reaches the model that actually answers it.
const normalized = text.toLowerCase();
const patterns = [/ignore.{0,40}instructions/, /reveal.{0,20}system prompt/];
Anchored on instruction-like phrasing specifically so an ordinary question doesn't get misclassified. A hit here skips the specialist model call entirely, but this only catches the patterns you thought to write, and it's worth being precise about which call it actually gates. In my own system, this prefilter blocks the intent-routing call, but a separate language-detection call runs in parallel and isn't gated by it; I only caught this by re-checking the claim against the code while writing this article. A blocked message's raw text can still reach a model, just not the one it was trying to manipulate. Know exactly which calls your guardrails gate before claiming they gate all of them.
4. Provider Safety Settings. Your prefilter catches what you thought to write a pattern for. The model provider's own content-safety thresholds are a second, independent layer for everything else. Neither layer alone is complete; together they cover each other's blind spots.
5. Output Guarding, Mid-Stream. The one that's easiest to miss. If you're streaming tokens as they arrive, a naive implementation has already shown the user half of a bad response before your check even runs. The fix is a small holdback buffer: never release the last 64 characters immediately, keep scanning the buffered text as new tokens arrive, and if something matches a block pattern, drop everything from that point and swap in a fixed fallback line.
Adding these in order taught me something I didn't expect going in: most of the actual work isn't the AI part. It's the boring functions sitting around it. The guardrails alone run to 174 lines, roughly the same size as the entire request handler that calls the model.
What Held Up, What Didn't
Applying each layer made the tradeoffs concrete rather than theoretical:
- The prefilter gates the specialist model call, so a blocked attempt skips the API call that would have answered it; it costs nothing beyond a regex check for that call specifically. It doesn't automatically gate every other call the request might trigger; check your own pipeline for calls running in parallel with the guarded one, not just after it.
- The rate limiter's honest limitation showed up immediately: restart the server and every counter resets. That's fine for stopping casual abuse; it's not a real defense, and pretending otherwise would be the mistake.
- The output guard's holdback buffer is a genuine tradeoff against latency: delaying the last 64 characters means the user's perceived "typing speed" trails the model's actual generation speed very slightly. I haven't measured exactly how perceptible that gap is at real network latency; it's a tradeoff worth testing for your own use case, not one to assume away.
What These Guardrails Don't Protect Against
None of this is a complete security story, and presenting it as one would be dishonest. Specific gaps worth naming:
- Distributed abuse. The rate limiter is an in-memory map per server instance. A botnet spread across thousands of IPs walks straight through it; the fix there is a shared store, not more cleverness on top of what's here.
- Regex bypasses. The prefilter catches known phrasings. A sufficiently motivated attacker rephrasing the same intent can get past it; this is pattern matching, not semantic understanding.
- Indirect prompt injection. If your feature ever summarizes or processes external content (a document, a webpage, a tool result), instructions hidden in that content are a different attack surface than anything covered here. None of these five layers inspect content the model retrieves after the fact.
- Tool or function-call abuse. If your chat feature can call tools or trigger actions, none of these layers validate what the model asks a tool to do. That needs its own authorization layer, independent of input/output filtering.
- Model hallucination. Nothing here checks whether the model's answer is factually correct. Guardrails constrain behavior (topic, safety, leakage), not accuracy.
- Credential and account-level abuse. Rate limiting by IP doesn't stop abuse from an authenticated account, or a leaked API key being used outside your application entirely.
The five layers in this article solve the problem they solve: keeping a scoped chat feature from being talked out of its scope, and from leaking things it shouldn't. They're not a substitute for authorization, content-provenance checks, or output verification where those actually apply.
Lessons Learned
- Validate before you spend tokens.
- Rate limit, and know exactly what it does and doesn't cover.
- Catch obvious injection attempts before the call they're meant to influence, and know exactly which calls that is, not which calls you assumed it was.
- Use the provider's safety layer too; it's not a replacement for your own.
- Guard the output stream, not just the input.
None of these five layers trust the model to behave. That's the actual difference between a demo and something you can put in front of strangers: not a cleverer prompt, but code around the prompt that doesn't need the model to cooperate in order to be safe.
