How I Handle Real-Time eSIM Provisioning Without a Backend Team
Quick Answer: I use message queues to decouple eSIM API requests from user-facing responses, webhook listeners for carrier status updates, and idempotent endpoints to prevent duplicate activations. This pattern lets a single developer manage hundreds of daily provisionings without building a backend team.
Introduction
When I first started building an eSIM marketplace, the biggest shock wasn't the complexity of carrier APIs—it was realizing I'd have to orchestrate real-time provisioning alone. No SRE team. No platform engineers. Just me and a VPS.
eSIM provisioning is deceptively simple in theory: a user buys a data plan, you generate a QR code, they scan it, they're online. In practice, you're juggling asynchronous carrier webhooks, retry storms, race conditions, and the existential dread of provisioning the same SIM twice.
This article is my field guide to keeping that infrastructure alive and sane without hiring backend engineers. It's not theory—it's what I shipped, broke, and fixed.
The Architecture Nobody Warned You About
What Makes eSIM Provisioning Hard?
eSIM (embedded SIM) provisioning involves remotely downloading a carrier profile to a device's embedded universal integrated circuit card (eUICC). Unlike traditional SIM cards that you physically swap, eSIM activation is a digital handshake between your platform, a carrier's SM-DP+ server, and the user's device.
The challenge? This handshake is asynchronous and failure-prone. A provisioning request might take anywhere from 2 seconds to 2 minutes. Carriers throttle requests. Devices go offline mid-provisioning. And every retry risks creating duplicate profiles that cost money and confuse users.
My Three-Pillar Approach
I settled on three architectural patterns that keep the system reliable:
- Message Queues: Decouple user-facing requests from carrier API calls
- Webhooks: Receive carrier status updates instead of polling
- Idempotency: Ensure duplicate requests never provision twice These aren't novel concepts, but combining them correctly for eSIM workflows requires specific decisions I'll walk through.
Pillar 1: Message Queues for Async Processing
Why Synchronous Provisioning Fails
Early on, I made the classic mistake of calling the carrier API directly during the HTTP request that handled the user's purchase. When the carrier API timed out after 30 seconds, the user's payment succeeded but their eSIM didn't provision. Worse, they couldn't retry because the payment was already processed.
The fix was decoupling with a message queue. Now the purchase endpoint immediately returns a "processing" status, enqueues a provisioning job, and responds to the user in under 200ms.
What I Actually Use
I use BullMQ (a Redis-based queue for Node.js) with three worker instances handling:
- Provisioning jobs: The actual carrier API calls
- Retry jobs: Failed provisionings with exponential backoff
-
Cleanup jobs: Stale pending activations after 24 hours
BullMQ handles rate limiting through its
rateLimitoption, which is critical because most carriers enforce strict request-per-minute quotas. I configure it based on each carrier's documented limits—some allow 60 RPM, others only 10.
Code Pattern: The Job Handler
Here's the pattern I follow for queue workers:
const provisionJob = await queue.add('provision', {
orderId: order.id,
carrier: 'global-esim',
profileType: 'data-plan-5gb',
idempotencyKey: `provision-${order.id}`
}, {
attempts: 5,
backoff: { type: 'exponential', delay: 2000 },
removeOnComplete: 100
});
The idempotencyKey here is crucial—I'll explain why in the third pillar.
Monitoring Queue Health
I track three metrics obsessively: queue depth (jobs waiting), processing latency (time from enqueue to completion), and dead letter count (jobs that exhausted retries). When queue depth spikes above 50, I get an alert. When dead letters accumulate, I investigate immediately.
The lesson: with message queues, visibility is everything. Without monitoring, you're just hoping jobs complete.
Pillar 2: Webhooks for Carrier Updates
Polling Is a Trap
My first instinct was to poll carrier APIs for provisioning status. Every 5 seconds, query the carrier: "Is it done yet?" This created a cascade of problems:
- I hit rate limits during peak hours
- I paid for API calls that returned the same "pending" status
- I couldn't scale beyond a few hundred daily orders Webhooks flipped the model. Instead of asking the carrier for updates, the carrier tells me when something changes.
Webhook Implementation Patterns
Carrier webhooks are notoriously unreliable. Some fire once and expect you to handle it. Others retry aggressively. A few send out-of-order updates. I built my webhook handler with these assumptions:
Assumption 1: Webhooks arrive out of order. I include a timestamp field in every webhook payload and only process updates that are newer than the last recorded state for that order.
Assumption 2: Webhooks retry indefinitely. I store processed webhook IDs in Redis with a 24-hour TTL to deduplicate identical payloads.
Assumption 3: Webhooks sometimes lie. I verify webhook signatures using the carrier's public key before trusting the payload. No signature verification means no state change.
Handling Webhook Failures
When my webhook endpoint returns a non-200 status, some carriers retry immediately, others wait 30 seconds, and a few give up after one attempt. I standardized my responses:
- 200 OK: Webhook processed successfully
- 202 Accepted: Webhook received but not yet actionable (e.g., waiting for prerequisite state)
- 4xx errors: Don't retry (malformed payload, invalid signature)
- 5xx errors: Retry is acceptable This gives carriers clear signals about what to do next, reducing both noise and missed updates.
What Happens When Webhooks Break
Carriers have outages too. When webhooks stop arriving, I fall back to a scheduled job that polls active provisionings every 5 minutes. This isn't my primary flow—it's my safety net. The key is keeping this fallback visible: I log every fallback poll so I know when webhooks are flaky.
Pillar 3: Idempotency for Safe Retries
The Duplicate Provisioning Nightmare
Without idempotency, retrying a failed provisioning creates duplicate eSIM profiles. Each profile costs money. Users end up with multiple QR codes. Support tickets multiply. This was my first major production incident.
Here's what happened: a carrier API returned a 500 error during provisioning. My queue retried the job. The carrier actually processed the first request but returned an error anyway. The retry created a second profile. The user had two active eSIMs for one purchase.
How Idempotency Keys Work
An idempotency key is a unique identifier attached to every provisioning request. If the carrier receives the same key twice, it processes the request once and returns the same response both times.
I generate keys using a deterministic pattern: provision-{orderId}-{attemptNumber}. For the first attempt: provision-123-1. If that fails and I retry, I use provision-123-2. This lets me distinguish between carrier failures (retry with new key) and network failures (retry with same key).
Idempotency at Every Layer
Idempotency isn't just for carrier APIs. I apply it throughout my stack:
- Database: UPSERT operations for order status updates
- Queue jobs: BullMQ's job ID prevents duplicate enqueues
- Webhook processing: Redis-based deduplication of webhook payloads
- User notifications: Notification IDs prevent duplicate emails The rule: if an operation can happen twice, it will. Design for it.
Error Recovery Without a Team
What Breaks and How I Fix It
Running infrastructure solo means you can't rotate on-call shifts. When something breaks at 3 AM, you handle it. Here's my recovery playbook:
Scenario 1: Carrier API is down. Jobs queue up but don't fail. I monitor queue depth and set an alert threshold. If a carrier is down for more than 30 minutes, I pause that carrier's queue and show users a maintenance message.
Scenario 2: Webhook endpoint is unreachable. My fallback polling catches missed updates. I fix the endpoint, then replay any affected orders manually using my admin dashboard.
Scenario 3: Database connection drops mid-provisioning. The job fails and retries. Because of idempotency keys, the carrier doesn't create duplicates. The retry succeeds once the database recovers.
Scenario 4: Race condition between webhook and job completion. I use database row-level locking (SELECT FOR UPDATE) when updating order status. The first update wins, the second sees the state change and exits cleanly.
The Admin Dashboard I Can't Live Without
I built a minimal admin dashboard that shows:
- Orders by status (pending, processing, completed, failed)
- Queue depth and worker status
- Recent webhook deliveries with payload previews
- Failed jobs with retry buttons This dashboard is my entire operations team. I can diagnose most issues in under 2 minutes without SSHing into servers.
What I'd Do Differently
Mistakes That Cost Me Time
Looking back, I'd make three changes:
1. Start with webhooks immediately. I spent two months polling before implementing webhooks. That was two months of unnecessary API costs and complexity.
2. Use structured logging from day one. I used console.log for too long. Switching to structured JSON logging (with correlation IDs spanning queue jobs, webhooks, and API calls) made debugging 10x faster.
3. Build the admin dashboard earlier. I operated through database queries for months. The dashboard wasn't optional infrastructure—it was essential tooling.
Key Takeaways
- Decouple with queues: Never call carrier APIs synchronously during user requests. Use message queues to handle async work.
- Trust but verify webhooks: Implement signature verification, timestamp ordering, and deduplication. Always have a polling fallback.
- Idempotency everywhere: If an operation can retry, it will. Use idempotency keys at API boundaries and deduplication everywhere else.
- Monitor queue health: Queue depth, latency, and dead letter metrics are your early warning system.
- Build operational tooling: A minimal admin dashboard is more valuable than perfect architecture when you're running solo.
- Plan for failure: Carriers fail, webhooks drop, databases disconnect. Design every component to recover gracefully.
Frequently Asked Questions
What is eSIM provisioning?
eSIM provisioning is the digital process of downloading a carrier profile to a device's embedded SIM chip, enabling cellular connectivity without a physical SIM card swap.
Why can't eSIM provisioning be synchronous?
Carrier APIs take 2 seconds to 2 minutes to process provisioning requests. Holding an HTTP connection open that long risks timeouts, poor user experience, and resource exhaustion.
What is an idempotency key?
An idempotency key is a unique identifier sent with API requests that ensures duplicate requests produce the same result without side effects, preventing duplicate eSIM activations.
How do I handle carrier API rate limits?
Use a queue with rate limiting configured to each carrier's documented requests-per-minute quota. BullMQ and similar libraries support this natively.
What if webhooks stop arriving?
Implement a fallback polling mechanism that checks active provisioning status every 5 minutes. Log all fallback polls to monitor webhook reliability.
How do I prevent duplicate eSIM profiles?
Use idempotency keys for carrier API requests, implement webhook deduplication with Redis, and use database UPSERTs for status updates.
Can one developer really manage this infrastructure?
Yes, with proper tooling: message queues for async processing, webhook handlers for status updates, idempotent APIs for safety, and a monitoring dashboard for visibility.
What's the best message queue for eSIM provisioning?
BullMQ (Redis-based) works well for Node.js applications. Alternatives include RabbitMQ, Apache Kafka, or cloud-native options like AWS SQS and Google Cloud Pub/Sub.
How do I monitor queue health?
Track queue depth (pending jobs), processing latency (enqueue to completion), and dead letter count (failed retries). Set alerts on thresholds that indicate problems.
Should I verify webhook signatures?
Yes, always verify webhook signatures using the carrier's public key. Unsigned or incorrectly signed webhooks should be rejected to prevent spoofed status updates.
Conclusion
Running real-time eSIM provisioning infrastructure as a solo developer isn't about cutting corners—it's about choosing the right abstractions. Message queues buy you time. Webhooks buy you efficiency. Idempotency buys you safety.
The architecture I've described here handles hundreds of daily provisionings without requiring a backend team. It's not perfect, but it's resilient enough that I sleep through the night.
If you're building something similar, start with queues and idempotency. Add webhooks when you're ready. And build that admin dashboard sooner than you think you need it.
P.S. If you're interested in eSIM infrastructure or want to see how I handle other parts of the stack, check out our homepage or browse our technical blog for more engineering write-ups. We also document our integration patterns for developers building eSIM-enabled products.
Top comments (0)