ezthrottle 1.1.1 โ†’ 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  The API Dam for rate-limited services. Queue and execute HTTP requests with smart retry logic, multi-region racing, and webhook delivery.
4
4
 
5
+ ## Get Your API Key
6
+
7
+ ๐Ÿ‘‰ **[Get started at ezthrottle.network](https://www.ezthrottle.network/)**
8
+
9
+ **Pay for delivery through outages and rate limiting. Unlimited free concurrency.**
10
+
11
+ No need to manage Lambda functions, SQS queues, DynamoDB, or complex retry logic. EZThrottle handles webhook fanout, distributed queuing, and multi-region orchestration for you. Just grab an API key and start shipping reliable API calls.
12
+
13
+ ### The End of Serverless Infrastructure
14
+
15
+ **RIP OPS. Hello serverless without maintenance.**
16
+
17
+ The era of managing serverless infrastructure is over. No more Lambda functions to deploy, SQS queues to configure, DynamoDB tables to provision, or CloudWatch alarms to tune. EZThrottle replaces your entire background job infrastructure with a single API call. Just code your business logicโ€”we handle the rest.
18
+
19
+ ### Speed & Reliability Through Multi-Region Racing
20
+
21
+ Execute requests across multiple geographic regions simultaneously (IAD, LAX, ORD, etc.). **The fastest region wins**โ€”delivering sub-second response times. When a region experiences issues, requests automatically route to healthy regions with zero configuration. Geographic distribution + intelligent routing = blazing-fast reliable delivery, every time.
22
+
5
23
  ## Installation
6
24
 
7
25
  ```bash
@@ -26,6 +44,303 @@ const result = await new Step(client)
26
44
  console.log(`Job ID: ${result.job_id}`);
27
45
  ```
28
46
 
47
+ ---
48
+
49
+ # Pricing
50
+
51
+ ## Free Tier - 1 Million Requests/Month Forever
52
+
53
+ **No credit card. No limits. All features included.**
54
+
55
+ - 1,000,000 requests per month FREE
56
+ - Multi-region racing, webhook fanout, retry logic - everything
57
+ - ~30,000 requests/day (covers most production apps)
58
+ - Perfect for indie devs, startups, side projects
59
+
60
+ ## Early Adopter Pricing (Subject to Change)
61
+
62
+ | Tier | Included Requests | Monthly Price | Overage (per 100k) | Hard Cap |
63
+ |------|------------------|---------------|-------------------|----------|
64
+ | **Free** | 1M requests/month | $0 | N/A | 1M (upgrade to continue) |
65
+ | **Indie** | 2M requests/month | $50 | $50/100k | 5M (upgrade to continue) |
66
+ | **Growth** | 5M requests/month | $200 | $40/100k | 10M (upgrade to continue) |
67
+ | **Pro** | 10M requests/month | $500 | $25/100k | 25M (upgrade to continue) |
68
+
69
+ **Hard caps protect you from surprise bills.** When you hit your tier's cap, requests pause until you upgrade or the month resets.
70
+
71
+ **Overage pricing:** Pay only for what you use beyond your included requests, up to your tier's hard cap.
72
+
73
+ **Example:** Indie tier uses 3M requests = $50 (base) + $50 (1M overage) = $100 total
74
+
75
+ ## Smart Upgrade Incentives
76
+
77
+ **The math makes upgrading obvious:**
78
+
79
+ **Scenario: Using 8M requests/month**
80
+
81
+ | Option | Calculation | Total Cost |
82
+ |--------|-------------|------------|
83
+ | Stay on Indie (hit cap) | Service stops at 5M | โŒ Lost revenue |
84
+ | Pay Indie overages | $50 + ($50 ร— 30) = $50 + $1,500 | โŒ $1,550/month |
85
+ | Upgrade to Growth | $200 base + ($40 ร— 30) = $200 + $1,200 | โš ๏ธ $1,400/month |
86
+ | Upgrade to Pro | $500 base (includes 10M) | โœ… $500/month |
87
+
88
+ **Upgrading to Pro saves you $900-1,050/month** vs paying overages.
89
+
90
+ **The tiers are designed so you WANT to upgrade** - overage pricing is intentionally expensive to make the next tier a no-brainer.
91
+
92
+ **Need 25M+ requests/month, no caps, or custom SLAs?**
93
+ ๐Ÿ‘‰ **[Contact us for enterprise pricing](https://www.ezthrottle.network/contact)**
94
+
95
+ ## Early Adopter Benefits
96
+
97
+ **Lock in these rates by signing up now.** Pricing subject to change for new customers. Early adopters keep their tier pricing even as we adjust rates.
98
+
99
+ **Questions?**
100
+ ๐Ÿ‘‰ **[Pricing FAQ](https://www.ezthrottle.network/pricing)** | **[Contact sales](https://www.ezthrottle.network/contact)**
101
+
102
+ **Ready to stop debugging Lambda at 3am?**
103
+ ๐Ÿ‘‰ **[Start free with 1M requests/month](https://www.ezthrottle.network/)**
104
+
105
+ ---
106
+
107
+ # Why This Pricing Makes Sense
108
+
109
+ ## What's a Good Night's Sleep Worth?
110
+
111
+ **3am PagerDuty alert:** "Stripe API down. Retry storm taking down prod. Revenue stopped."
112
+
113
+ You wake up. Laptop. VPN. SSH into servers. Lambda logs scrolling. DynamoDB throttling. SQS backlog exploding. IAM policies denying for no reason. Concurrent execution limits hit. CloudWatch costs spiking.
114
+
115
+ You spend 2 hours debugging. Fix the immediate issue. Write a post-mortem. Promise to "build better retry logic."
116
+
117
+ **Three months later, same alert. Different API.**
118
+
119
+ ---
120
+
121
+ ## The AWS Nightmare Nobody Talks About
122
+
123
+ **Building retry infrastructure on AWS means:**
124
+
125
+ **Lambda Hell:**
126
+ - Concurrent execution limits (1000 by default, need to request increases)
127
+ - Cold starts killing performance (500ms+ latency spikes)
128
+ - IAM policies that randomly deny for no fucking reason
129
+ - CloudWatch logs costing more than the Lambdas themselves
130
+ - Debugging distributed traces across 47 Lambda invocations
131
+
132
+ **SQS Madness:**
133
+ - Dead letter queues filling up
134
+ - Visibility timeout confusion (did it process? who knows!)
135
+ - FIFO vs Standard (wrong choice = data loss)
136
+ - Poison messages breaking your workers
137
+ - No built-in retry logic for 429/500 errors
138
+
139
+ **DynamoDB Pain:**
140
+ - Provisioned throughput math (always wrong)
141
+ - Hot partition keys throttling randomly
142
+ - GSI limits (20 max, need to plan carefully)
143
+ - Point-in-time recovery costing $$$
144
+ - Read/write capacity units (what even are these?)
145
+
146
+ **The Real Kicker:**
147
+ - **AWS has no built-in tool for queueing 429 and 500 errors at scale**
148
+ - You have to build it yourself
149
+ - With Lambda + SQS + DynamoDB + Step Functions + EventBridge
150
+ - And debug the whole mess when it breaks at 3am
151
+
152
+ ---
153
+
154
+ ## Why AWS Can't Do This (And EZThrottle Can)
155
+
156
+ **Performance:**
157
+ - **EZThrottle core:** Written in Gleam (compiles to Erlang/OTP)
158
+ - **Actor-based concurrency:** Millions of jobs, zero race conditions
159
+ - **Sub-millisecond job routing:** Faster than Lambda cold starts
160
+ - **Multi-region racing:** Native to our architecture (not bolted on)
161
+
162
+ **AWS Stack:**
163
+ - Lambda: Cold starts, concurrent execution limits, IAM hell
164
+ - SQS: No native retry logic, visibility timeout confusion
165
+ - DynamoDB: Hot partitions, throughput throttling
166
+ - Step Functions: $0.025 per 1000 state transitions (adds up fast)
167
+
168
+ **You can't build this on AWS serverless and get the same performance.**
169
+ We tried. It doesn't work. That's why we built EZThrottle.
170
+
171
+ ---
172
+
173
+ ## The Hidden Cost of Retry Storms
174
+
175
+ **What happens when Stripe/OpenAI/Anthropic has an outage?**
176
+
177
+ ### Without EZThrottle:
178
+
179
+ **5-minute API outage causes:**
180
+ ```
181
+ 1000 req/sec ร— 5 retries = 5000 req/sec retry storm
182
+ 5000 req/sec ร— 300 seconds = 1.5M failed requests
183
+ 1.5M ร— 10KB payload = 15GB egress
184
+ 15GB ร— $0.09/GB = $1,350 in AWS egress fees
185
+
186
+ Plus:
187
+ - Lambda concurrent execution limit hit (all new requests fail)
188
+ - SQS queues backing up (visibility timeout chaos)
189
+ - DynamoDB throttling (hot partition from retry attempts)
190
+ - CloudWatch logs exploding ($200+ in 5 minutes)
191
+ - Your servers maxed out (can't serve real users)
192
+
193
+ Total cost: $1,550 + 2 hours of engineer time + lost revenue
194
+ ```
195
+
196
+ ### With EZThrottle:
197
+
198
+ **Same 5-minute outage:**
199
+ ```
200
+ 1000 req/sec ร— 1 submit to EZThrottle = 1000 req/sec
201
+ 300k requests ร— $0.50/1k = $150 total
202
+
203
+ Plus:
204
+ - Your servers stay healthy (serving real users)
205
+ - No retry storm (EZThrottle handles retries)
206
+ - No egress fees (one request out, webhook back)
207
+ - No debugging at 3am
208
+ - No lost revenue
209
+
210
+ Total cost: $150 + 0 engineer time + 0 lost revenue
211
+ ```
212
+
213
+ **Savings: $1,400 per outage** (and your sanity)
214
+
215
+ ---
216
+
217
+ ## The Hidden Cost of Building This Yourself
218
+
219
+ **You're about to hire 2 engineers to build retry infrastructure. Let's do the math.**
220
+
221
+ ### DIY Cost (AWS + Engineers):
222
+
223
+ | Component | Year 1 | Ongoing |
224
+ |-----------|--------|---------|
225
+ | **Infrastructure** | | |
226
+ | Lambda (retries + webhooks) | $1,200 | $1,200/year |
227
+ | SQS (job queues) | $1,200 | $1,200/year |
228
+ | DynamoDB (state tracking) | $3,000 | $3,000/year |
229
+ | CloudWatch (logs) | $1,200 | $1,200/year |
230
+ | Data transfer (egress fees) | $12,000 | $12,000/year |
231
+ | **Infrastructure subtotal** | **$18,600** | **$18,600/year** |
232
+ | | | |
233
+ | **Engineering** | | |
234
+ | Initial build (3 months, 2 engineers @ $150k) | $75,000 | - |
235
+ | Ongoing maintenance (30% time, 2 engineers) | $45,000 | $90,000/year |
236
+ | On-call rotation (outage response) | $15,000 | $30,000/year |
237
+ | **Engineering subtotal** | **$135,000** | **$120,000/year** |
238
+ | | | |
239
+ | **TOTAL DIY COST** | **$153,600** | **$138,600/year** |
240
+
241
+ ### EZThrottle Cost:
242
+
243
+ | Component | Year 1 | Ongoing |
244
+ |-----------|--------|---------|
245
+ | Free tier (1M requests/month) | $0 | $0/year |
246
+ | Pro tier (2M requests/month) | $6,000 | $6,000/year |
247
+ | Engineer time to integrate | $5,000 | $0/year |
248
+ | **TOTAL EZTHROTTLE COST** | **$11,000** | **$6,000/year** |
249
+
250
+ **Savings: $142,600 in Year 1, $132,600/year ongoing**
251
+
252
+ Or put another way: **You save an entire senior engineer's salary every year.**
253
+
254
+ ---
255
+
256
+ ## FRUGAL vs PERFORMANCE: Choose Your Strategy
257
+
258
+ | Feature | FRUGAL | PERFORMANCE |
259
+ |---------|--------|-------------|
260
+ | **Execution** | Client-side first | Server-side distributed |
261
+ | **When to use** | High success rate (95%+) | Mission-critical / high traffic |
262
+ | **Cost** | Only pay when forwarded | Always uses EZThrottle |
263
+ | **During API outages** | Retry storm (melts your servers) | Servers stay healthy |
264
+ | **Egress fees** | High (every retry = AWS egress) | Low (one request to EZThrottle) |
265
+ | **Lambda limits** | Hit concurrent execution cap | Never hit limits |
266
+ | **IAM debugging** | Your problem | Not your problem |
267
+ | **Good night's sleep** | Nope | Yes |
268
+
269
+ ### Rate Limiting: 2 RPS Per Domain
270
+
271
+ EZThrottle throttles at **2 requests per second PER TARGET DOMAIN**:
272
+
273
+ - `api.stripe.com` โ†’ 2 RPS
274
+ - `api.openai.com` โ†’ 2 RPS
275
+ - `api.anthropic.com` โ†’ 2 RPS
276
+
277
+ All domains run concurrently. The limit is per destination, not per account.
278
+
279
+ **Need higher limits?** Return `X-EZTHROTTLE-RPS` header or [request custom defaults](https://github.com/rjpruitt16/ezconfig).
280
+
281
+ ---
282
+
283
+ ## Real-World Example: Payment Processor
284
+
285
+ **Before EZThrottle (AWS Lambda + SQS):**
286
+ - Stripe outage: 15 minutes
287
+ - Retry storm: 2M failed requests
288
+ - AWS egress fees: $1,800
289
+ - Lambda concurrent execution limit hit: 45 minutes total downtime
290
+ - Lost revenue: $50,000
291
+ - Engineer time debugging: 6 hours (including 3am wake-up)
292
+ - CloudWatch logs: $400
293
+ - Customer support tickets: 200
294
+ - **Total cost per outage: $52,200**
295
+
296
+ **After EZThrottle:**
297
+ - Same Stripe outage: 15 minutes
298
+ - Submitted to EZThrottle: 300k requests
299
+ - EZThrottle cost: $150
300
+ - Servers stayed online: 0 minutes downtime
301
+ - Lost revenue: $0
302
+ - Engineer time: 0 hours (slept through it)
303
+ - Customer support tickets: 5
304
+ - **Total cost per outage: $150**
305
+
306
+ **ROI: 348x cost reduction per outage**
307
+
308
+ Plus ongoing savings:
309
+ - 60% reduction in AWS egress fees ($7,200/year saved)
310
+ - Zero Lambda IAM debugging (priceless)
311
+ - No more 3am pages (actually priceless)
312
+ - One less engineer needed ($150k/year saved)
313
+
314
+ ---
315
+
316
+ ## What You're Really Paying For
317
+
318
+ โŒ **Wrong comparison:** "EZThrottle ($500/1M) vs Lambda ($0.20/1M)"
319
+ โ†’ This ignores SQS, DynamoDB, egress, IAM hell, and engineers
320
+
321
+ โœ… **Right comparison:** "EZThrottle ($6k/year) vs DIY ($139k/year)"
322
+ โ†’ Lambda + SQS + DynamoDB + engineers + sanity
323
+
324
+ **You're not paying for request proxying.**
325
+ **You're paying to never debug Lambda IAM policies at 3am again.**
326
+
327
+ **What you get:**
328
+ - โœ… No retry storms during API outages
329
+ - โœ… No Lambda concurrent execution limits
330
+ - โœ… No IAM policy debugging hell
331
+ - โœ… No SQS dead letter queue mysteries
332
+ - โœ… No DynamoDB hot partition throttling
333
+ - โœ… Multi-region racing (3+ regions, fastest wins)
334
+ - โœ… Webhook reliability (automatic retries)
335
+ - โœ… Built in Gleam/OTP (actor-based, zero race conditions)
336
+ - โœ… Sleep through outages (we handle it)
337
+
338
+ **AWS can't do this at this scale. That's why EZThrottle exists.**
339
+
340
+ ---
341
+
342
+ # SDK Documentation
343
+
29
344
  ## Step Types
30
345
 
31
346
  ### StepType.PERFORMANCE (Server-side execution)
@@ -195,6 +510,273 @@ await new Step(client)
195
510
  .execute();
196
511
  ```
197
512
 
513
+ ## Rate Limiting & Tuning
514
+
515
+ EZThrottle intelligently manages rate limits for your API calls. By default, requests are throttled at **2 RPS (requests per second)** to smooth rate limiting across distributed workers and prevent API overload.
516
+
517
+ ### Dynamic Rate Limiting via Response Headers
518
+
519
+ Your API can communicate rate limits back to EZThrottle using response headers:
520
+
521
+ ```javascript
522
+ // Your API responds with these headers:
523
+ X-EZTHROTTLE-RPS: 5 // Allow 5 requests per second
524
+ X-EZTHROTTLE-MAX-CONCURRENT: 10 // Allow 10 concurrent requests
525
+ ```
526
+
527
+ **Header Details:**
528
+ - `X-EZTHROTTLE-RPS`: Requests per second (e.g., `0.5` = 1 request per 2 seconds, `5` = 5 requests per second)
529
+ - `X-EZTHROTTLE-MAX-CONCURRENT`: Maximum concurrent requests (default: 2 per machine)
530
+
531
+ EZThrottle automatically adjusts its rate limiting based on these headers, ensuring optimal throughput without overwhelming your APIs.
532
+
533
+ **Performance Note:** Server-side retry handling is significantly faster and more performant than client-side retry loops. EZThrottle's distributed architecture eliminates connection overhead and retry latency. *Benchmarks coming soon.*
534
+
535
+ ### Requesting Custom Defaults
536
+
537
+ Need different default rate limits for your account? Submit a configuration request:
538
+
539
+ ๐Ÿ‘‰ **[Request custom defaults at github.com/rjpruitt16/ezconfig](https://github.com/rjpruitt16/ezconfig)**
540
+
541
+ ## Webhook Payload
542
+
543
+ When EZThrottle completes your job, it sends a POST request to your webhook URL with the following JSON payload:
544
+
545
+ ```json
546
+ {
547
+ "job_id": "job_1763674210055_853341",
548
+ "idempotent_key": "custom_key_or_generated_hash",
549
+ "status": "success",
550
+ "response": {
551
+ "status_code": 200,
552
+ "headers": {
553
+ "content-type": "application/json"
554
+ },
555
+ "body": "{\"result\": \"data\"}"
556
+ },
557
+ "metadata": {}
558
+ }
559
+ ```
560
+
561
+ **Fields:**
562
+ - `job_id` - Unique identifier for this job
563
+ - `idempotent_key` - Your custom key or auto-generated hash
564
+ - `status` - `"success"` or `"failed"`
565
+ - `response.status_code` - HTTP status code from the target API
566
+ - `response.headers` - Response headers from the target API
567
+ - `response.body` - Response body from the target API (as string)
568
+ - `metadata` - Custom metadata you provided during job submission
569
+
570
+ **Example webhook handler (Express):**
571
+ ```javascript
572
+ const express = require('express');
573
+ const app = express();
574
+
575
+ app.use(express.json());
576
+
577
+ app.post('/webhook', (req, res) => {
578
+ const payload = req.body;
579
+
580
+ const jobId = payload.job_id;
581
+ const status = payload.status;
582
+
583
+ if (status === 'success') {
584
+ const responseBody = payload.response.body;
585
+ // Process successful result
586
+ console.log(`Job ${jobId} succeeded:`, responseBody);
587
+ } else {
588
+ // Handle failure
589
+ console.log(`Job ${jobId} failed`);
590
+ }
591
+
592
+ res.json({ ok: true });
593
+ });
594
+ ```
595
+
596
+ ## Webhook Security (HMAC Signatures)
597
+
598
+ Protect webhooks from spoofing with HMAC-SHA256 signature verification.
599
+
600
+ ### Quick Setup
601
+
602
+ ```typescript
603
+ import express from 'express';
604
+ import { EZThrottle, verifyWebhookSignatureStrict, WebhookVerificationError } from 'ezthrottle';
605
+
606
+ const app = express();
607
+ const client = new EZThrottle({ apiKey: 'your_api_key' });
608
+ const WEBHOOK_SECRET = 'your_secret_min_16_chars';
609
+
610
+ // Create secret (one time)
611
+ await client.createWebhookSecret('your_secret_min_16_chars');
612
+
613
+ // Verify webhooks
614
+ app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
615
+ try {
616
+ verifyWebhookSignatureStrict(
617
+ req.body,
618
+ req.headers['x-ezthrottle-signature'] as string,
619
+ WEBHOOK_SECRET
620
+ );
621
+
622
+ const data = JSON.parse(req.body.toString());
623
+ console.log(`Job ${data.job_id}: ${data.status}`);
624
+ res.json({ ok: true });
625
+ } catch (error) {
626
+ if (error instanceof WebhookVerificationError) {
627
+ return res.status(401).json({ error: error.message });
628
+ }
629
+ throw error;
630
+ }
631
+ });
632
+ ```
633
+
634
+ ### Verification Functions
635
+
636
+ ```typescript
637
+ import { verifyWebhookSignature, tryVerifyWithSecrets } from 'ezthrottle';
638
+
639
+ // Boolean verification
640
+ const { verified, reason } = verifyWebhookSignature(payload, signature, secret);
641
+ if (!verified) console.log(`Failed: ${reason}`);
642
+
643
+ // Secret rotation support
644
+ const result = tryVerifyWithSecrets(
645
+ payload,
646
+ signature,
647
+ 'new_secret',
648
+ 'old_secret' // Optional
649
+ );
650
+ console.log(result.reason); // "valid_primary" or "valid_secondary"
651
+ ```
652
+
653
+ ### Manage Secrets
654
+
655
+ ```typescript
656
+ // Create/update
657
+ await client.createWebhookSecret('primary_secret', 'secondary_secret');
658
+
659
+ // Get (masked)
660
+ const secrets = await client.getWebhookSecret();
661
+ // { primary_secret: 'prim****cret', has_secondary: true }
662
+
663
+ // Rotate safely
664
+ await client.rotateWebhookSecret('new_secret');
665
+
666
+ // Delete
667
+ await client.deleteWebhookSecret();
668
+ ```
669
+
670
+ ### Quick Commands (One-Liners)
671
+
672
+ ```bash
673
+ # Create secret
674
+ node -e "const {EZThrottle}=require('ezthrottle'); new EZThrottle({apiKey:'key'}).createWebhookSecret('secret').then(console.log)"
675
+
676
+ # Get secrets
677
+ node -e "const {EZThrottle}=require('ezthrottle'); new EZThrottle({apiKey:'key'}).getWebhookSecret().then(r=>console.log(JSON.stringify(r,null,2)))"
678
+
679
+ # Rotate secret
680
+ node -e "const {EZThrottle}=require('ezthrottle'); new EZThrottle({apiKey:'key'}).rotateWebhookSecret('new_secret').then(console.log)"
681
+
682
+ # Delete
683
+ node -e "const {EZThrottle}=require('ezthrottle'); new EZThrottle({apiKey:'key'}).deleteWebhookSecret().then(console.log)"
684
+
685
+ # With env var
686
+ export EZTHROTTLE_API_KEY="your_key"
687
+ node -e "const {EZThrottle}=require('ezthrottle'); new EZThrottle({apiKey:process.env.EZTHROTTLE_API_KEY}).createWebhookSecret('secret').then(console.log)"
688
+ ```
689
+
690
+ ### Best Practices
691
+
692
+ 1. Always verify signatures in production
693
+ 2. Use 32+ character random secrets
694
+ 3. Rotate secrets periodically with primary + secondary
695
+ 4. Store secrets in environment variables
696
+
697
+ ## Mixed Workflow Chains (FRUGAL โ†” PERFORMANCE)
698
+
699
+ Mix FRUGAL and PERFORMANCE steps in the same workflow to optimize for both cost and speed:
700
+
701
+ ### Example 1: FRUGAL โ†’ PERFORMANCE (Save money, then fast delivery)
702
+
703
+ ```javascript
704
+ // Primary API call is cheap (local execution)
705
+ // But notification needs speed (multi-region racing)
706
+ const result = await new Step(client)
707
+ .url('https://api.openai.com/v1/chat/completions')
708
+ .type(StepType.FRUGAL) // Execute locally first
709
+ .fallbackOnError([429, 500])
710
+ .onSuccess(
711
+ // Chain to PERFORMANCE for fast webhook delivery
712
+ new Step(client)
713
+ .url('https://api.sendgrid.com/send')
714
+ .type(StepType.PERFORMANCE) // Distributed execution
715
+ .webhooks([{ url: 'https://app.com/email-sent' }])
716
+ .regions(['iad', 'lax', 'ord'])
717
+ )
718
+ .execute();
719
+ ```
720
+
721
+ ### Example 2: PERFORMANCE โ†’ FRUGAL (Fast payment, then cheap analytics)
722
+
723
+ ```javascript
724
+ // Critical payment needs speed (racing)
725
+ // But analytics is cheap (local execution when webhook arrives)
726
+ const payment = await new Step(client)
727
+ .url('https://api.stripe.com/charges')
728
+ .type(StepType.PERFORMANCE) // Fast distributed execution
729
+ .webhooks([{ url: 'https://app.com/payment-complete' }])
730
+ .regions(['iad', 'lax'])
731
+ .onSuccess(
732
+ // Analytics doesn't need speed - save money!
733
+ new Step(client)
734
+ .url('https://analytics.com/track')
735
+ .type(StepType.FRUGAL) // Client executes when webhook arrives
736
+ )
737
+ .execute();
738
+ ```
739
+
740
+ ### Example 3: Complex Mixed Workflow
741
+
742
+ ```javascript
743
+ // Optimize every step for its requirements
744
+ const workflow = await new Step(client)
745
+ .url('https://cheap-api.com')
746
+ .type(StepType.FRUGAL) // Try locally first
747
+ .fallbackOnError([429, 500])
748
+ .fallback(
749
+ new Step().url('https://backup-api.com'), // Still FRUGAL
750
+ { triggerOnError: [500] }
751
+ )
752
+ .onSuccess(
753
+ // Critical notification needs PERFORMANCE
754
+ new Step(client)
755
+ .url('https://critical-webhook.com')
756
+ .type(StepType.PERFORMANCE)
757
+ .webhooks([{ url: 'https://app.com/webhook' }])
758
+ .regions(['iad', 'lax', 'ord'])
759
+ .onSuccess(
760
+ // Analytics is cheap again
761
+ new Step(client)
762
+ .url('https://analytics.com/track')
763
+ .type(StepType.FRUGAL)
764
+ )
765
+ )
766
+ .onFailure(
767
+ // Simple Slack alert doesn't need PERFORMANCE
768
+ new Step(client)
769
+ .url('https://hooks.slack.com/webhook')
770
+ .type(StepType.FRUGAL)
771
+ )
772
+ .execute();
773
+ ```
774
+
775
+ **Why mix workflows?**
776
+ - โœ… **Cost optimization** - Only pay for what needs speed
777
+ - โœ… **Performance where it matters** - Critical paths get multi-region racing
778
+ - โœ… **Flexibility** - Every step optimized for its specific requirements
779
+
198
780
  ## Production Ready โœ…
199
781
 
200
782
  This SDK is production-ready with **working examples validated in CI on every push**.
@@ -257,6 +839,244 @@ await new Step(client)
257
839
  - โœ… 7 integration tests covering all SDK features
258
840
  - โœ… Proves the code actually works, not just documentation
259
841
 
842
+ ## Legacy Code Integration (executeWithForwarding)
843
+
844
+ Integrate EZThrottle into existing codebases without refactoring error handling. Return `{ forward: ForwardRequest }` from your legacy functions to trigger automatic forwarding to EZThrottle.
845
+
846
+ ```javascript
847
+ const { executeWithForwarding, StepType } = require('ezthrottle');
848
+
849
+ // Legacy function that may hit rate limits
850
+ async function processPayment(orderId) {
851
+ try {
852
+ const response = await fetch('https://api.stripe.com/charges', {
853
+ method: 'POST',
854
+ headers: { 'Authorization': `Bearer ${STRIPE_KEY}` },
855
+ body: JSON.stringify({ amount: 1000, currency: 'usd' })
856
+ });
857
+
858
+ if (response.status === 429) {
859
+ // Rate limited - return ForwardRequest to auto-forward to EZThrottle
860
+ return {
861
+ forward: {
862
+ url: 'https://api.stripe.com/charges',
863
+ method: 'POST',
864
+ idempotentKey: `order_${orderId}`,
865
+ webhooks: [{ url: 'https://app.com/webhook', hasQuorumVote: true }],
866
+ stepType: StepType.FRUGAL
867
+ }
868
+ };
869
+ }
870
+
871
+ return await response.json();
872
+ } catch (error) {
873
+ // Network error - auto-forward to EZThrottle
874
+ return {
875
+ forward: {
876
+ url: 'https://api.stripe.com/charges',
877
+ method: 'POST',
878
+ idempotentKey: `order_${orderId}`
879
+ }
880
+ };
881
+ }
882
+ }
883
+
884
+ // Wrap with auto-forwarding
885
+ const result = await executeWithForwarding(client, () => processPayment('order_123'));
886
+ console.log(result); // Either direct response or EZThrottle job metadata
887
+ ```
888
+
889
+ ### Decorator-Style Wrapper
890
+
891
+ Create wrapped functions that automatically forward on errors:
892
+
893
+ ```javascript
894
+ const { withAutoForward } = require('ezthrottle');
895
+
896
+ // Wrap once
897
+ const processPaymentWithForwarding = withAutoForward(client, processPayment);
898
+
899
+ // Use everywhere
900
+ const result1 = await processPaymentWithForwarding('order_123');
901
+ const result2 = await processPaymentWithForwarding('order_456');
902
+ ```
903
+
904
+ ## Async/Await Streaming (Non-Blocking Webhook Waiting)
905
+
906
+ Wait for webhook results asynchronously without blocking your application. Perfect for workflows that need to continue processing while waiting for EZThrottle to complete jobs.
907
+
908
+ ### Basic Async Example
909
+
910
+ ```javascript
911
+ const { Step, StepType } = require('ezthrottle');
912
+
913
+ async function processWithWebhook() {
914
+ // Submit job to EZThrottle
915
+ const result = await new Step(client)
916
+ .url('https://api.example.com/endpoint')
917
+ .method('POST')
918
+ .type(StepType.PERFORMANCE)
919
+ .webhooks([{ url: 'https://app.com/webhook', hasQuorumVote: true }])
920
+ .idempotentKey('async_job_123')
921
+ .execute();
922
+
923
+ console.log(`Job submitted: ${result.job_id}`);
924
+
925
+ // Continue processing while EZThrottle executes the job
926
+ // Your webhook endpoint will receive the result asynchronously
927
+ }
928
+
929
+ // Non-blocking execution
930
+ processWithWebhook().then(() => {
931
+ console.log('Job submission complete, continuing with other work...');
932
+ });
933
+ ```
934
+
935
+ ### Concurrent Job Submission
936
+
937
+ Submit multiple jobs concurrently and process results as they arrive:
938
+
939
+ ```javascript
940
+ async function processBatchConcurrently(orders) {
941
+ // Submit all jobs concurrently
942
+ const promises = orders.map(async (order) => {
943
+ const result = await new Step(client)
944
+ .url(`https://api.example.com/process`)
945
+ .method('POST')
946
+ .body(JSON.stringify(order))
947
+ .type(StepType.PERFORMANCE)
948
+ .webhooks([{ url: 'https://app.com/webhook', hasQuorumVote: true }])
949
+ .idempotentKey(`order_${order.id}`)
950
+ .execute();
951
+
952
+ return {
953
+ orderId: order.id,
954
+ jobId: result.job_id,
955
+ idempotentKey: result.idempotent_key
956
+ };
957
+ });
958
+
959
+ // Wait for all submissions to complete
960
+ const submissions = await Promise.all(promises);
961
+
962
+ console.log(`Submitted ${submissions.length} jobs concurrently`);
963
+ submissions.forEach(s => {
964
+ console.log(`Order ${s.orderId} โ†’ Job ${s.jobId}`);
965
+ });
966
+
967
+ // Webhook results will arrive asynchronously at https://app.com/webhook
968
+ return submissions;
969
+ }
970
+
971
+ // Example usage
972
+ const orders = [
973
+ { id: 'order_1', amount: 1000 },
974
+ { id: 'order_2', amount: 2000 },
975
+ { id: 'order_3', amount: 3000 }
976
+ ];
977
+
978
+ processBatchConcurrently(orders).then(submissions => {
979
+ console.log('All jobs submitted, processing continues...');
980
+ });
981
+ ```
982
+
983
+ ### Promise.allSettled for Fault Tolerance
984
+
985
+ Handle failures gracefully when submitting multiple jobs:
986
+
987
+ ```javascript
988
+ async function processBatchWithErrorHandling(orders) {
989
+ const promises = orders.map(async (order) => {
990
+ try {
991
+ const result = await new Step(client)
992
+ .url(`https://api.example.com/process`)
993
+ .method('POST')
994
+ .body(JSON.stringify(order))
995
+ .type(StepType.PERFORMANCE)
996
+ .webhooks([{ url: 'https://app.com/webhook', hasQuorumVote: true }])
997
+ .idempotentKey(`order_${order.id}`)
998
+ .execute();
999
+
1000
+ return { orderId: order.id, jobId: result.job_id };
1001
+ } catch (error) {
1002
+ return { orderId: order.id, error: error.message };
1003
+ }
1004
+ });
1005
+
1006
+ // Wait for all promises to settle (success or failure)
1007
+ const results = await Promise.allSettled(promises);
1008
+
1009
+ const succeeded = results.filter(r => r.status === 'fulfilled' && !r.value.error);
1010
+ const failed = results.filter(r => r.status === 'rejected' || r.value?.error);
1011
+
1012
+ console.log(`Succeeded: ${succeeded.length}, Failed: ${failed.length}`);
1013
+
1014
+ return { succeeded, failed };
1015
+ }
1016
+ ```
1017
+
1018
+ ### Integration with Express Webhook Handler
1019
+
1020
+ ```javascript
1021
+ const express = require('express');
1022
+ const app = express();
1023
+
1024
+ app.use(express.json());
1025
+
1026
+ // In-memory store for webhook results (use Redis/DB in production)
1027
+ const webhookResults = new Map();
1028
+
1029
+ // Webhook receiver
1030
+ app.post('/webhook', (req, res) => {
1031
+ const { job_id, idempotent_key, status, response } = req.body;
1032
+
1033
+ // Store result for polling or processing
1034
+ webhookResults.set(idempotent_key, {
1035
+ jobId: job_id,
1036
+ status,
1037
+ response,
1038
+ receivedAt: new Date()
1039
+ });
1040
+
1041
+ console.log(`Webhook received for ${idempotent_key}: ${status}`);
1042
+
1043
+ res.json({ ok: true });
1044
+ });
1045
+
1046
+ // Submit job and continue processing
1047
+ app.post('/submit', async (req, res) => {
1048
+ const idempotentKey = `job_${Date.now()}`;
1049
+
1050
+ const result = await new Step(client)
1051
+ .url('https://api.example.com/endpoint')
1052
+ .method('POST')
1053
+ .type(StepType.PERFORMANCE)
1054
+ .webhooks([{ url: 'https://app.com/webhook', hasQuorumVote: true }])
1055
+ .idempotentKey(idempotentKey)
1056
+ .execute();
1057
+
1058
+ // Return immediately, don't wait for webhook
1059
+ res.json({
1060
+ jobId: result.job_id,
1061
+ idempotentKey: idempotentKey,
1062
+ message: 'Job submitted, webhook will arrive asynchronously'
1063
+ });
1064
+ });
1065
+
1066
+ // Poll for webhook result
1067
+ app.get('/result/:idempotentKey', (req, res) => {
1068
+ const result = webhookResults.get(req.params.idempotentKey);
1069
+
1070
+ if (result) {
1071
+ res.json({ found: true, result });
1072
+ } else {
1073
+ res.json({ found: false, message: 'Webhook not yet received' });
1074
+ }
1075
+ });
1076
+
1077
+ app.listen(3000, () => console.log('Server listening on port 3000'));
1078
+ ```
1079
+
260
1080
  ## Legacy API (Deprecated)
261
1081
 
262
1082
  For backward compatibility, the old `queueRequest()` method is still available:
@@ -271,6 +1091,10 @@ await client.queueRequest({
271
1091
 
272
1092
  **Prefer the new `Step` builder API for all new code!**
273
1093
 
1094
+ ---
1095
+
1096
+ # Appendix
1097
+
274
1098
  ## Environment Variables
275
1099
 
276
1100
  ```bash