Edge Functions

Recursive / Nested Function Calls

Understanding rate limits when Edge Functions invoke each other


Edge Functions can call other Edge Functions using fetch(). This enables powerful patterns like function chaining, fan-out/fan-in workflows, and recursive processing. To protect platform stability and prevent runaway amplification, Supabase rate limits these internal function-to-function calls.

What gets rate limited#

Rate limiting applies to outbound fetch() calls made by your Edge Functions to other Edge Functions within your project. This includes:

  • Direct recursion: A function calling itself
  • Function chaining: Function A calling Function B
  • Circular calls: Function A calling Function B, which calls Function A
  • Fan-out patterns: A function calling multiple other functions concurrently

Rate limit budget#

Each request chain has a budget of at least 5,000 requests per minute. In busier regions, this budget may be higher. All function-to-function calls within the same request chain share this budget.

For example, if Function A calls Function B, and Function B calls Function C, all three calls count toward the same budget pool.

Handling rate limit errors#

When the rate limit is exceeded, calling another Edge Function throws a RateLimitError. This error includes a retryAfterMs property indicating how long to wait (in milliseconds) before retrying. You should catch this error and handle it gracefully:

1
import { createClient } from 'jsr:@supabase/supabase-js@2'
2
3
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')!)
4
5
Deno.serve(async (req) => {
6
try {
7
const { data, error } = await supabase.functions.invoke('other-function', {
8
body: { foo: 'bar' },
9
})
10
11
if (error) throw error
12
13
return new Response(JSON.stringify(data), {
14
headers: { 'Content-Type': 'application/json' },
15
})
16
} catch (err) {
17
if (err instanceof Deno.errors.RateLimitError) {
18
// Use retryAfterMs to tell the client when to retry
19
const retryAfterSeconds = Math.ceil(err.retryAfterMs / 1000)
20
return new Response(
21
JSON.stringify({ error: 'Service temporarily unavailable. Please retry later.' }),
22
{
23
status: 429,
24
headers: {
25
'Content-Type': 'application/json',
26
'Retry-After': retryAfterSeconds.toString(),
27
},
28
}
29
)
30
}
31
throw err
32
}
33
})

You can also use retryAfterMs to implement automatic retries within your function:

1
import { createClient } from 'jsr:@supabase/supabase-js@2'
2
3
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')!)
4
5
async function invokeWithRetry(functionName: string, payload: object, maxRetries = 3) {
6
for (let attempt = 0; attempt < maxRetries; attempt++) {
7
try {
8
const { data, error } = await supabase.functions.invoke(functionName, {
9
body: payload,
10
})
11
if (error) throw error
12
return data
13
} catch (err) {
14
if (err instanceof Deno.errors.RateLimitError && attempt < maxRetries - 1) {
15
// Wait for the recommended duration before retrying
16
await new Promise((resolve) => setTimeout(resolve, err.retryAfterMs))
17
continue
18
}
19
throw err
20
}
21
}
22
}

Tips for avoiding rate limits#

1. Batch operations instead of individual calls#

Instead of calling a function once per item, batch multiple items into a single call:

1
// ❌ Avoid: One call per item
2
for (const item of items) {
3
await supabase.functions.invoke('process-item', { body: item })
4
}
5
6
// ✅ Better: Batch items into one call
7
await supabase.functions.invoke('process-items', { body: { items } })

2. Limit recursion depth#

If your function is recursive, set a maximum depth to prevent unbounded call chains:

1
Deno.serve(async (req) => {
2
const { depth = 0, data } = await req.json()
3
4
if (depth >= 5) {
5
// Stop recursion at max depth
6
return new Response(JSON.stringify({ result: data }))
7
}
8
9
// Process and recurse with incremented depth
10
const processed = processData(data)
11
const { data: result } = await supabase.functions.invoke('my-function', {
12
body: { depth: depth + 1, data: processed },
13
})
14
15
return new Response(JSON.stringify(result))
16
})

3. Use queues for large workloads#

For processing large datasets, consider using Supabase Queues instead of recursive function calls. Queues handle backpressure automatically and are better suited for high-volume workloads.

4. Use shared libraries instead of separate functions#

Instead of creating separate Edge Functions that call each other, create a shared library of functions and import them directly. This avoids HTTP overhead and rate limits entirely:

1
// supabase/functions/_shared/transform.ts
2
export function (: any) {
3
// validation logic
4
}
5
6
export function (: any) {
7
// transformation logic
8
}
9
10
export async function (: any) {
11
// save logic
12
}
1
// supabase/functions/process-data/index.ts
2
import { validate, transform, save } from '../_shared/transform.ts'
3
4
Deno.serve(async (req) => {
5
const data = await req.json()
6
const validated = validate(data)
7
const transformed = transform(validated)
8
const result = await save(transformed)
9
return new Response(JSON.stringify(result))
10
})

5. Add delays for non-urgent processing#

If immediate processing isn't required, add delays between calls to spread the load:

1
async function processWithDelay(items: any[]) {
2
for (const item of items) {
3
await supabase.functions.invoke('process-item', { body: item })
4
await new Promise((resolve) => setTimeout(resolve, 100)) // 100ms delay
5
}
6
}

Common patterns and their impact#

PatternBudget consumptionRecommendation
Simple chain (A to B to C)LowGenerally safe
Fan-out (A to B, C, D, E)ModerateLimit concurrency
Deep recursion (A to A to A...)HighSet max depth
Unbounded loopsVery highAvoid, use queues

Increasing rate limits#

Currently, all plans have the same rate limit budget. We are working on introducing custom limits for different use cases.

If you need a higher rate limit for your project, contact support with details about your use case.