Webhooks vs polling: building real-time exam notifications for dental imaging workflows
Every dental imaging integration eventually faces the same architectural question: how does the referring dentist's app find out a CBCT scan is ready? If your answer is "we poll the API every 30 seconds", you're shipping latency, burning serverless invocations and draining mobile batteries. There's a better default — event-driven webhooks with HMAC verification and idempotent retries — and this post is the case for switching.
The two patterns in one paragraph
Polling means your code asks "is it ready yet?" on a timer. Webhooks mean CBCTHub calls your endpoint the moment an event happens. Polling is pull, webhooks are push. Both work. They have very different cost, latency and operational profiles.
Side-by-side comparison
| Dimension | Polling (every 30s) | Webhooks |
|---|---|---|
| Median latency | 15 s (half the interval) | < 1 s |
| Worst-case latency | 30 s + retry budget | seconds, with retries on top |
| Cloud invocations per 100 exams (assuming 10 min upload) | ~2,000 | ~100 |
| Mobile battery impact | High — wakes the radio every interval | None — server pushes |
| Behind a firewall / on a laptop | Works fine | Needs an inbound URL |
| Debuggability | Trivial — replay any request | Needs delivery logs + replay UI |
| Backpressure | Natural — you control the rate | Provider must implement queueing |
For typical PMS ↔ CBCTHub flows where you want the dentist's app to update the second the exam is ready, webhooks win on every dimension that matters to end users.
When polling still makes sense
Webhooks aren't a religion. Reach for polling when:
- You're debugging locally and don't want to tunnel ngrok/Cloudflare into your laptop.
- Your system can't expose an inbound URL — for example, a legacy PMS deployed on a clinic LAN with no public IP and no reverse proxy budget.
- You batch results periodically — a nightly cron that imports the day's exams works fine on polling.
- The event is rare and you already poll on a UX trigger (e.g., when the user opens the exam list).
Everything else — real-time dentist notifications, mobile push relays, status displays in the waiting room — belongs on webhooks.
How CBCTHub signs webhooks
Every webhook delivery includes two headers:
X-CBCTHub-Signature: HMAC-SHA256 of the raw request body, signed with the secret you saw once when you created the subscription.X-CBCTHub-Delivery: a unique ID for this delivery attempt. Use it as your idempotency key.
Verify the signature before doing anything else. If verification fails, return 401 and log the attempt — it's either a misconfigured secret or someone fishing for a vulnerability.
Node.js / Express verification
import crypto from 'crypto';
import express from 'express';
const app = express();
app.post(
'/cbcthub/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.header('X-CBCTHub-Signature') ?? '';
const expected = crypto
.createHmac('sha256', process.env.CBCTHUB_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (
sig.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
) {
return res.status(401).send('invalid signature');
}
const deliveryId = req.header('X-CBCTHub-Delivery');
const event = JSON.parse(req.body.toString());
// Idempotency — return 200 if we've seen this delivery before
if (await deliveries.has(deliveryId)) return res.status(200).send('duplicate');
await deliveries.add(deliveryId);
await handleEvent(event); // your business logic
return res.status(200).send('ok');
}
);
Python / FastAPI verification
import hmac, hashlib, os
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
SECRET = os.environ['CBCTHUB_WEBHOOK_SECRET'].encode()
@app.post('/cbcthub/webhook')
async def webhook(req: Request):
body = await req.body()
sig = req.headers.get('x-cbcthub-signature', '')
expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
raise HTTPException(401, 'invalid signature')
delivery_id = req.headers.get('x-cbcthub-delivery')
event = await req.json()
if await deliveries.contains(delivery_id):
return {'status': 'duplicate'}
await deliveries.add(delivery_id)
await handle_event(event)
return {'status': 'ok'}
Idempotency: the rule you cannot skip
CBCTHub retries deliveries that don't receive a 2xx response, with exponential backoff for up to 24 hours. That means your endpoint will receive the same event more than once eventually, either because a 5xx leaked or because the response timed out. Use X-CBCTHub-Delivery as a deduplication key in Redis, a database table, or even an LRU cache, and return 200 on the second attempt without re-running side effects.
Test mode: validate without touching production
Generate a cbct_test_ API key, register a webhook against it, and trigger a test event from the dashboard. Events fired with a test key carry "livemode": false in their payload, so your code can route them to a staging database while still running the same handler. This is the safest way to ship a webhook integration without risking a real exam delivery going to the wrong inbox.
Pattern: webhooks → durable queue → workers
The most robust production pattern keeps your webhook handler small: verify signature, enqueue, return 200. A separate worker pool consumes the queue and does the slow work. This isolates webhook acknowledgement from your business logic, so a database hiccup never causes CBCTHub to back off.
Concrete stacks:
- Vercel: webhook route → Inngest or QStash → background functions.
- AWS: API Gateway → SQS → Lambda consumers.
- Self-hosted: Express → Redis stream → BullMQ workers.
- Cloudflare: Worker → Queue → consumer Worker.
When to choose what
Default to webhooks for any real-time, user-facing event. Keep polling around for nightly imports, debugging tools and legacy on-prem systems that can't accept inbound traffic. The hybrid is fine — many production integrations use both, with webhooks as the primary path and a daily reconciliation poll as a safety net.
Wrap up
Polling is the easy first answer and the wrong long-term one. HMAC-verified webhooks with idempotent handlers and a small queue cost less, scale better and give end users a real-time experience. The CBCTHub webhook delivery system is built for this — automatic retries, replay UI, livemode separation — so the only thing left for you to do is build the receiver.
Try CBCTHub for free
Upload, view, and share DICOM scans in the cloud. Nothing to install.
Create free accountRelated articles
How to integrate your dental practice management software with CBCTHub: a complete API tutorial
Step-by-step guide to integrating a dental PMS with the CBCTHub REST API. Upload exams, embed the 3D viewer, and automate delivery in Node, Python and PHP.
HIPAA-compliant dental imaging APIs: security checklist for US healthcare integrations
A 10-point HIPAA security checklist for any dental imaging API integration in the US. Encryption, BAAs, audit logs, breach notification, patient rights.
Best CBCT Viewers 2026: Complete Comparison of 10 Dental Imaging Software Options
In-depth comparison of the best CBCT viewers for dental imaging in 2026. Cloud-based and desktop options, pricing, features, and best-fit scenarios.