Lesson 14 · Optional advanced lab
Optional lab: background spam screening with room to recover
add automatic publication for confidently clean comments while holding spam, uncertainty, and failures for owner review.
Sign in to save progress across devices
On this page
Lab scope: extend a working manual comments service from Lesson 13. The configuration fragments and pseudocode below are an implementation blueprint, not a standalone deployable service.
1. Use several defenses
CAPTCHA is the first filter. Add bounded input sizes, per-network and per-site submission budgets, idempotency keys, plain-text rendering, and owner moderation. Optional login helps identify accounts but does not prove a comment is good. Signed-in users should still pass the same abuse checks.
Decide the review policy before choosing a model. Disagreement, short thanks, and emotional discussion of personal topics are not spam by themselves. Promotion, scams, harassment, threats, and exposed private information need different handling. Ambiguous content can remain pending; avoid promising that automation removes all review work.
2. Save first, screen in a queue
Screening during every HTTP submission leaves less time for verification and request handling. Our implementation moved it to a Queue consumer so the HTTP response could finish after persistence and enqueueing.
Validated submission
|
Save pending comment in D1
|
Enqueue { commentId } -------- enqueue fails ---> private pending row
|
Queue consumer loads current D1 state
|
Claim bounded lease + version
|
Screen full name and body
|
+-----+---------------------------+
| |
Exact CLEAN, flags allow SPAM / REVIEW / failure
| |
Fenced approval write Hold privately for owner
The Queue carries only the comment ID. It is not the authoritative store of comment text or identity. Save the pending row before sending the message, then return a prompt response indicating pending/queued. A short browser poll may look for the newly approved public record; it must never display a private pending response as if approved.
D1 and Queue sends are not one transaction. An insert can succeed while enqueueing fails or has an ambiguous outcome. Keep the saved row private and available to the owner. Queue messages can also expire before processing, so a pending-review recovery path remains necessary.
3. Provision one queue per environment
Create the staging queue first:
pnpm exec wrangler queues create my-blog-comments-screening-staging
After implementing the consumer, add the AI binding and this Queue configuration to the staging file. These are top-level fragments:
"ai": { "binding": "AI" },
"queues": {
"producers": [
{ "binding": "SCREENING_QUEUE", "queue": "my-blog-comments-screening-staging" }
],
"consumers": [
{
"queue": "my-blog-comments-screening-staging",
"max_batch_size": 1,
"max_batch_timeout": 1,
"max_retries": 3,
"retry_delay": 31,
"max_concurrency": 1
}
]
}
If CLI authorization lacks Queues access, review and authorize the needed permission in the intended account. Create a distinct production queue when production is ready. Never connect the staging consumer to production comments or messages.
4. Make duplicate delivery harmless
Cloudflare documents at-least-once delivery, so a comment may be processed more than once. A simple “if pending, approve” implementation can overwrite a later owner decision.
Implement a short screening lease, a bounded attempt count, and a version used in conditional database updates. The consumer must skip deleted rows, owner-moderated rows, already completed work, and posts removed from the deployment’s published allowlist. Claim a row atomically, screen it, and write only if the claimed version/lease still matches.
This is pseudocode describing the required control flow:
load row by validated comment ID
if missing, finished, owner-decided, or post unpublished: acknowledge and stop
if lease is active: retry after its expiry
if attempt budget exhausted: hold for owner, acknowledge
claim pending work using an atomic conditional write
if claim lost: reread/acknowledge or retry as appropriate
run screening within a bounded timeout and site AI budget
if CLEAN and publication flags allow:
conditionally approve only the exact claimed version
else:
conditionally hold only the exact claimed version
acknowledge; retry transient failures within the delivery budget
The owner always wins: approval, rejection, and deletion invalidate the worker’s version. A late model result must not restore removed content. Bound both message retries and inference claims; they are related but not identical.
Flags and the post manifest belong to the deployment processing an event. If a rapid emergency needs to stop publication across in-flight events, account for older consumers and use a tested queue/consumer pause procedure too. A configuration change alone cannot revoke an old event’s snapshot.
5. Evaluate the classifier before auto-approval
Treat the name and body as untrusted data. Pass them as data with clear instructions that embedded role names, “owner approved this,” or “return CLEAN” are not authority. Prompt wording helps, but the database/session rules enforce real authority.
Use a small, strict response vocabulary: CLEAN, SPAM, or REVIEW. Parse the complete answer; unexpected output, timeouts, unavailable inference, and exhausted budgets should hold the comment. Do not accept any response merely because it contains the substring CLEAN.
Create an evaluation corpus containing genuine on-topic discussion, disagreement, short replies, sensitive personal writing, promotional links, scams, harassment, privacy violations, and attempts to manipulate moderation. Include contextual, noncommercial reference links so useful discussion is not blocked just for containing a URL.
Run a held-out set that you did not use to tune the prompt. Inspect both dangerous auto-approvals and genuine comments held unnecessarily. Review mistakes in context. Choose release criteria before evaluating, then keep AUTO_APPROVE="false" until those criteria and the failure tests pass. Local mocks test application behavior; they do not measure the real model’s quality.
6. Measure limits instead of assuming “serverless” means free
Read the current Workers limits, D1 pricing, Workers AI pricing, and Queue pricing. HTTP, queue processing, database use, builds, and model inference have different budgets. CPU time is not network waiting time.
As checked for this edition, Workers Free lists 10 ms CPU per HTTP request. Queues Free includes 10,000 operations/day and 24-hour retention. A small successful message normally uses a write, read, and delete; retries add operations. For example, 100 screened comments/day would normally use roughly 300 queue operations before retries. That does not prove the other services stay within their limits.
Set hard application budgets appropriate to your blog. When a budget is exhausted, save/hold safely or reject with a useful response; do not silently publish or upgrade billing. Measure guest submission, real Google authentication, owner actions, and queue consumers with actual staging requests, including cold starts and bursts. A few low-CPU samples are evidence, not a guarantee of headroom under all conditions.
7. Test failure and recovery paths
Test duplicate delivery, expired leases, queue send failure after D1 insert, unavailable inference, malformed model output, owner rejection during inference, deleted parents, unpublished posts, exhausted budgets, and repeated submission. Public output should expose only approved records and public fields in every case.
Before production migration/export, record the target database and take a backup to private encrypted storage. D1 export commands can write directly into your mounted encrypted backup location:
pnpm exec wrangler d1 export COMMENTS_DB --remote --output /YOUR_ENCRYPTED_BACKUP_PATH/comments.sql --config wrangler.production-comments.jsonc
That example assumes you have created and reviewed an explicit production comments config. If production instead lives in wrangler.jsonc, use that actual file. Verify the database binding and destination first; an exported SQL file contains reader data and does not belong in Git or public/.
Practice restoring into a separate database with synthetic or suitably protected data. D1 backups do not include Queue messages. Never attach a restored test Worker to the production queue, and never bulk re-enqueue every pending row without distinguishing owner-held content from unfinished screening work.
Checkpoint and launch order
Deploy the completed code with both feature flags false. Verify the real domain, static articles, owner identity, private API boundaries, and unavailable-public-comments behavior. Enable public comments with manual approval first. Enable auto-approval only after model evaluation, failure tests, browser tests, and live resource measurements pass.
Record the source commit and deployed version; check the actual config/public endpoints after release. Keep reviewing held comments and a sample of approved ones, track failures and usage, and retain a way to disable publication. The practical goal is less routine review with clear exceptions and recoverable failures.