Troubleshooting
Start here
Almost every report falls into one of four shapes. Find yours.
| Symptom | Usually |
|---|---|
| The email arrives, but Submissions is empty | The Twig tag is missing |
| Nothing arrives and nothing is stored | Contact Form itself, not Bandage |
| Nothing arrives but it is stored, marked spam | The score reached the threshold |
| Real people are being caught | The time trap or a blocklist, usually |
The submission screen shows the score and every check that contributed to it. That list is the answer to most of these questions, so look there before changing settings.
Submissions is empty, but the email arrived
Contact Form sent the message without Bandage ever seeing it, which means the form has no
{{ craft.bandage.form() }} in it. View source on the live page and look for a hidden input named
bandageForm. If it isn't there, the tag isn't in the template that actually rendered.
Watch for a second, older contact template that nobody remembers — a footer form, a modal, a
landing page — still posting to contact-form/send without the tag.
If the input is there, check Store submissions in the plugin settings and Store on the form profile. Either being off is a deliberate no-record configuration.
Nothing arrives and nothing is stored
Bandage never ran, so the problem is upstream of it. In order:
- Is Contact Form itself installed and enabled?
php craft plugin/install contact-form. - Does Contact Form have a
toEmail? Without one, and without recipients on the Bandage form profile, there is nowhere to send to. - Is the form failing validation?
submission.getErrors()will say. A missing or malformedfromEmailnever reaches the send step. - Check Craft's mail settings and the queue.
A failure to store never fails a send, by design — the person who filled in the form doesn't know Bandage exists, and losing their enquiry to an archive would be the wrong trade. So "stored but not sent" and "sent but not stored" are different faults with different causes.
It's stored, marked spam, and it shouldn't be
Open it. The score breakdown names every check that fired.
Honeypot fired on a real person. Password managers fill hidden fields. That is exactly why the
default score is 10 against a threshold of 5 rather than an outright veto — but if it happens
repeatedly, rename the honeypot field in the plugin settings. Some managers key off the field name.
Time trap fired. Two directions:
- Too fast — the minimum is
3seconds by default. A form above the fold on a page somebody has already read once is genuinely submitted in two. Lower it. - Too slow — the maximum is
86400seconds. A form left open in a tab overnight trips it. Raise it, or set it to0for no upper bound.
Blocked words. They match anywhere in the submission, including inside longer words and inside
email addresses. A blocked word of cash catches somebody at cashman.co.uk.
Require MX fired. The sender's domain has no MX record. Genuinely misconfigured domains exist, and so do domains that only accept mail through a provider that answers slowly. This check costs a DNS lookup on every submission; turn it off if it is causing more trouble than it stops.
CAPTCHA fired. See below.
To recover a message: Mark as not spam on the submission, then Resend. Nothing was destroyed — every spam action stores the message, which is the reason quarantine is the default.
Spam is getting through
Look at what the delivered spam scored. If it scored 0, none of your enabled checks saw anything;
if it scored 4 against a threshold of 5, you are one point away.
In rough order of how much they help per unit of risk:
- Turn on rate limiting —
5per hour per IP stops volume without judging content. - Turn on link counting at
3or so. The cheapest signal there is. - Add a CAPTCHA. Turnstile is the least intrusive of the four.
- Lower the threshold to
4, and watch the flagged submissions for a week.
Use the flag action while tuning. It sends everything and marks what it would have caught, so you can see what a threshold change would have done to real traffic before it does it.
Turn on Require MX and Disposable email last. They are the two most likely to catch a real person.
CAPTCHA problems
Nothing renders. The site key is empty, or the template passes { captcha: false }, or the
form profile the template names isn't the one you configured. Check the hidden bandageForm value
against the handle in the CP.
Everything is scored as a CAPTCHA failure. The secret is wrong, or belongs to a different site key, or the domain isn't on the provider's allow-list.
reCAPTCHA v3 catches real people. Lower the minimum score. 0.5 is Google's suggestion, not a
law; 0.3 is a reasonable floor for a contact form.
Verification fails open if the provider can't be reached at all and closed on a returned failure. So a provider outage will not block your enquiries — and if enquiries are being blocked, the provider answered, and the answer was no.
Field validation isn't firing
Field rules are Pro. On Lite the rules are saved and not applied.
If you're on Pro:
- The rule's field is a value path, not an input name.
message[Phone]in the template ismessage.Phonein the rule. - Every rule type except required passes on an empty value. Add a
requiredrule alongside it. - Contact Form's own errors come first. If
fromEmailis invalid, Bandage's rules are skipped entirely that time round rather than burying the one error the visitor can act on.
Routing isn't routing
- Routing is Pro.
- Rules run in order, and the first match stops unless you turn that off. A catch-all rule with no conditions matches everything — put it last, or it will match first.
- Check the condition field spelling:
message.Department, notDepartment, and notmessage[Department]. - The submission screen records the addresses it actually used. Compare them to what you expected before assuming the rule didn't match.
The autoresponder isn't sending
- It is Pro, and it is off by default.
- It is rate limited to
3per hour per recipient address. Testing repeatedly with your own address hits that cap on the fourth try, which looks exactly like it stopped working. - If the autoresponder is conditional, check the conditions the same way as routing.
- It only fires on a message that was actually sent. Quarantined spam never reaches that step.
Webhooks aren't arriving
- Pro, and delivered on the queue — so check the queue is running. A stalled queue looks like a broken webhook.
- The endpoint must be
http://orhttps://. Anything else is refused at save. - Spam is excluded by default. Turn on include spam if you meant to receive it.
- Signature mismatches on the receiving end are almost always the body being re-serialised before it is verified. The HMAC is over the exact bytes sent, not over a re-encoded object.
Attachments aren't saved
- Attachments are off by default, and need a volume.
- A file over the size limit, past the count limit, or with a disallowed extension produces a visible error on the form rather than a silent drop.
- Extensions fall back to Craft's
allowedFileExtensionswhen the list is empty, so a file Craft won't accept anywhere is refused here too.
Everything broke after a deploy
Forms are project config. If a form disappeared, it was removed from project.yml somewhere and
applied here. The submissions it took are unaffected — formHandle is stored as a snapshot string
precisely so that deleting a form never takes its correspondence with it.
Still stuck
Craft's logs carry a bandage category. Take the submission's reference, the score breakdown and
the log lines around that timestamp, and email
justin@justinholt.com.
Next
- Configuration — every setting and what it defaults to
- FAQ — editions, privacy, and what happens if you remove it