← Back to blog
Product September 21, 2026 by Javier Arancibia

blurd — a self-hosted service that redacts faces and plates, and never keeps the original

A client needed to push photos through a pipeline without ever retaining the personal data in them. Faces, licence plates — the GDPR surface of a street-level image. Every option I found either wanted my images in someone else's cloud, or was a library that left me to build the service around it: the queue, the API keys, the dashboard, the multi-tenant isolation, the retention policy.

So I built blurd. One binary. You POST an image (bytes, or a URL it fetches itself), it detects faces and plates with pretrained ONNX models, redacts them, stores only the redacted output — and reduces the source to a SHA-256. The original never touches disk. Not in a queue, not in a temp file, nowhere. That single constraint ends up driving most of the design.

blurd dashboard — stats, filter facets and a grid of redacted thumbnails

The API a machine actually wants

Submission is asynchronous because HTTP won't hold for a batch: you get a job id, you poll or long-poll with ?wait=. Retrieval is by your own identifier — a filename, an object key — not an internal id you have to store:

curl -X POST http://blurd:8770/v1/images \
  -H "X-Api-Key: $KEY" -F "file=@photo.jpg" \
  -F "code=cam3/IMG_0042.jpg" -F "tags=site:paris"

# later, the consumer side:
curl http://blurd:8770/v1/blobs/by-code/cam3/IMG_0042.jpg -H "X-Api-Key: $KEY"

Scoped API keys are tenants, not filters. Two apps can use the same filename for different images and never see each other's data — an out-of-scope read is a 404, never a 403, because a 403 confirms the resource exists.

v0.20.0: the parts a demo deployment needs

  • Public blob rules — an admin declares "tag X is public" in the dashboard and /pub/blobs/<sha> serves those images with no key. Sha-addressed only (a sha256 isn't enumerable; a filename is), revoked the moment the rule is deleted.
  • Rate limiting — 60/min on public paths, 300/min on authenticated, per IP, with grouped rate-limit events in the admin UI and a 30-day audit retention so a long-lived instance doesn't grow on traffic alone.
  • A hard storage capBLURD_STORAGE_MAX_BYTES bounds live blob bytes. Over the cap, expired TTL blobs are reclaimed first, then writes fail with a typed 507 storage_full. For cheap demo VMs this is the difference between "full disk" and "backpressure".
  • TTL outputs — profile-level storage.ttl: the blob expires, the thumbnail and the record survive, the dashboard renders an expired card instead of a hole.

What running it against real data taught me

I pointed a test instance at ~900 real production images (~126 MB). Detection itself is fast — p50 130 ms/image — and every surprise was elsewhere: broken uploads in the source dataset, upstream 404s, and one genuine bug I was glad to find in test rather than prod: two workers processing the same image raced the artifact insert and the loser died as an internal_error. All three races of that shape are fixed; a six-way concurrent submission is now a conformance check.

Also worth knowing: the job listing now filters by source-sha prefix and by submission tag — "show me the failed jobs of batch genbatch=real-100mb" is one query.

The honest limitations

Detection recall is not 100% and blurd says so — artifacts with no detections, or any detection under 0.55 confidence, are flagged needs_review for a human. It is a strong first pass, not a compliance guarantee. And the Helm chart has never been applied to a live cluster; it renders, lints and passes conformance under Docker, and that's the claim.

The whole thing is agent-first: JSON on stdout, a typed error vocabulary shared between CLI and HTTP, and 154 black-box conformance checks that run identically against SQLite, PostgreSQL and MongoDB. The spec/ directory carries the design rationale if you want the why before the code.

Try it: git clone https://github.com/javimosch/blurd && ./demo.sh brings up the service plus a sidecar standing in for your producer and consumer apps, credentials printed.

Enjoyed this post?

Follow for more on agent-first engineering, self-hosted systems, and building for autonomy.

Follow @javimosch