A Moscow medical clinic was losing money every day. 21% of appointments ended in no-shows — patients who booked but never came. Staff spent hours on manual reminder calls. The booking process was 100% phone-based.
They hired me to fix it. Here's what I built and what happened in the first 5 weeks.
The actual business problem
No-shows in medical clinics aren't just annoying — they're expensive. An empty slot that could have been filled is pure revenue loss. At this clinic, 21% no-show rate translated to roughly $7,200/month in lost revenue.
The root causes were straightforward:
- Patients forgot appointments (no automated reminders)
- Rebooking required a phone call during working hours
- Staff had no visibility into who was likely to no-show
Architecture
- Bot: Python 3.12 + aiogram 3 (Telegram)
- Backend: FastAPI + PostgreSQL 15 + Redis
- AI: GigaChat API (Russian language, data stays in Russia — required for medical data under 152-FZ)
- Vector search: ChromaDB + rubert-tiny2 embeddings
- EHR integration: 1C:Медицина REST API with circuit breaker
- Billing integration: 1C:Бухгалтерия OData
- Scheduling: APScheduler with 12 jobs
- Notifications: React PWA + Web Push
- Compliance: 152-FZ (Russian medical data law), AES-256 encryption via pgcrypto, LUKS disk encryption
GigaChat instead of OpenAI was a hard requirement — Russian medical regulations prohibit sending patient data to foreign servers.
The reminder system
The core of the no-show reduction was a multi-touch reminder sequence:
T-48h: "You have an appointment in 2 days. Confirm or reschedule?"
T-24h: "Reminder: tomorrow at 14:00 with Dr. Ivanova. Reply to confirm."
T-2h: "Your appointment is in 2 hours. We're expecting you."
T+30m: If no-show detected → automatic slot release + waitlist notification
Each message has inline keyboard buttons: Confirm / Reschedule / Cancel. Cancellations automatically trigger waitlist fulfillment.
AI triage
Patients can describe symptoms in free text. The bot uses a RAG pipeline (ChromaDB + rubert-tiny2 + GigaChat) to match symptoms against a curated medical knowledge base and suggest the appropriate specialist.
async def triage_patient(symptom_text: str) -> TriageResult:
embeddings = await encoder.encode(symptom_text)
similar_cases = await chroma.query(embeddings, n_results=5)
prompt = build_triage_prompt(symptom_text, similar_cases)
response = await gigachat.complete(prompt)
return parse_triage_result(response)
Accuracy after 5 weeks: 89.7% correct specialist routing (validated against actual doctor assignments).
1C integration headaches
1C:Медицина has a REST API that's... not great. Endpoints time out randomly. The solution was a circuit breaker pattern:
- 3 consecutive failures → circuit opens, fallback to cached data
- 30-second cooldown → circuit half-opens, single probe request
- Success → circuit closes, normal operation resumes
Without this, a 1C outage would have taken down the entire booking flow.
Compliance (152-FZ)
Russian medical data law is strict:
- All patient data must be stored on Russian servers (Selectel VPS)
- Encryption at rest required (pgcrypto AES-256 + LUKS)
- Audit log for every data access
- Data retention and deletion procedures
This added roughly 40 hours to the project but was non-negotiable.
Results after 5 weeks
| Metric | Before | After |
|---|---|---|
| No-show rate | 21% | 8.1% |
| Online bookings | 0% | 74% |
| Monthly recovered revenue | — | +$7,200 |
| Patients in system | 0 | 1,134 |
| Total appointments | 0 | 1,847 |
The clinic recovered the development cost in under 3 weeks.
What I'd do differently
Build the admin panel earlier. Clinic staff needed dashboards from day one — I delivered them in week 3. Earlier delivery would have accelerated adoption.
Stress test the APScheduler jobs. With 12 scheduled jobs running simultaneously at peak hours, there were occasional delays. Moving to Celery Beat for the high-priority reminder jobs would be cleaner at scale.
More granular no-show prediction. The current system sends reminders to everyone equally. A simple ML model scoring no-show probability per patient (based on history, appointment type, day of week) would let you prioritize aggressive reminders on high-risk slots.
Stack summary
| Layer | Technology |
|---|---|
| Bot | Python 3.12, aiogram 3 |
| Backend | FastAPI, APScheduler |
| Database | PostgreSQL 15 + pgcrypto, Redis |
| AI | GigaChat API, ChromaDB, rubert-tiny2 |
| Integrations | 1C:Медицина, 1C:Бухгалтерия |
| Frontend | React PWA, Web Push |
| Infrastructure | Selectel VPS, LUKS, Docker |
| Compliance | 152-FZ |
Questions about the architecture, GigaChat integration, or 1C circuit breaker pattern — ask in the comments.
Top comments (0)