apps/docs/content/guides/functions/recursive-functions.mdx
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.
Rate limiting applies to outbound fetch() calls made by your Edge Functions to other Edge Functions within your project. This includes:
Inbound requests to your Edge Functions and requests to external APIs (e.g., Stripe, OpenAI) are not subject to this rate limit. Only outbound calls from one Edge Function to another Edge Function are counted.
</Admonition>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.
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:
<Tabs scrollable size="small" type="underlined" defaultActiveId="supabase-js" queryGroup="client"
<TabPanel id="supabase-js" label="supabase-js">
import { createClient } from 'jsr:@supabase/supabase-js@2'
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')!)
Deno.serve(async (req) => {
try {
const { data, error } = await supabase.functions.invoke('other-function', {
body: { foo: 'bar' },
})
if (error) throw error
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' },
})
} catch (err) {
if (err instanceof Deno.errors.RateLimitError) {
// Use retryAfterMs to tell the client when to retry
const retryAfterSeconds = Math.ceil(err.retryAfterMs / 1000)
return new Response(
JSON.stringify({ error: 'Service temporarily unavailable. Please retry later.' }),
{
status: 429,
headers: {
'Content-Type': 'application/json',
'Retry-After': retryAfterSeconds.toString(),
},
}
)
}
throw err
}
})
Deno.serve(async (req) => {
try {
const response = await fetch(`${Deno.env.get('SUPABASE_URL')}/functions/v1/other-function`, {
method: 'POST',
headers: {
Authorization: `Bearer ${Deno.env.get('SUPABASE_ANON_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ foo: 'bar' }),
})
return response
} catch (err) {
if (err instanceof Deno.errors.RateLimitError) {
// Use retryAfterMs to tell the client when to retry
const retryAfterSeconds = Math.ceil(err.retryAfterMs / 1000)
return new Response(
JSON.stringify({ error: 'Service temporarily unavailable. Please retry later.' }),
{
status: 429,
headers: {
'Content-Type': 'application/json',
'Retry-After': retryAfterSeconds.toString(),
},
}
)
}
throw err
}
})
You can also use retryAfterMs to implement automatic retries within your function:
<Tabs scrollable size="small" type="underlined" defaultActiveId="supabase-js" queryGroup="client"
<TabPanel id="supabase-js" label="supabase-js">
import { createClient } from 'jsr:@supabase/supabase-js@2'
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')!)
async function invokeWithRetry(functionName: string, payload: object, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const { data, error } = await supabase.functions.invoke(functionName, {
body: payload,
})
if (error) throw error
return data
} catch (err) {
if (err instanceof Deno.errors.RateLimitError && attempt < maxRetries - 1) {
// Wait for the recommended duration before retrying
await new Promise((resolve) => setTimeout(resolve, err.retryAfterMs))
continue
}
throw err
}
}
}
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fetch(url, options)
} catch (err) {
if (err instanceof Deno.errors.RateLimitError && attempt < maxRetries - 1) {
// Wait for the recommended duration before retrying
await new Promise((resolve) => setTimeout(resolve, err.retryAfterMs))
continue
}
throw err
}
}
}
Instead of calling a function once per item, batch multiple items into a single call:
// ❌ Avoid: One call per item
for (const item of items) {
await supabase.functions.invoke('process-item', { body: item })
}
// ✅ Better: Batch items into one call
await supabase.functions.invoke('process-items', { body: { items } })
If your function is recursive, set a maximum depth to prevent unbounded call chains:
Deno.serve(async (req) => {
const { depth = 0, data } = await req.json()
if (depth >= 5) {
// Stop recursion at max depth
return new Response(JSON.stringify({ result: data }))
}
// Process and recurse with incremented depth
const processed = processData(data)
const { data: result } = await supabase.functions.invoke('my-function', {
body: { depth: depth + 1, data: processed },
})
return new Response(JSON.stringify(result))
})
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.
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:
// supabase/functions/_shared/transform.ts
export function validate(data: any) {
// validation logic
}
export function transform(data: any) {
// transformation logic
}
export async function save(data: any) {
// save logic
}
// supabase/functions/process-data/index.ts
import { validate, transform, save } from '../_shared/transform.ts'
Deno.serve(async (req) => {
const data = await req.json()
const validated = validate(data)
const transformed = transform(validated)
const result = await save(transformed)
return new Response(JSON.stringify(result))
})
If immediate processing isn't required, add delays between calls to spread the load:
async function processWithDelay(items: any[]) {
for (const item of items) {
await supabase.functions.invoke('process-item', { body: item })
await new Promise((resolve) => setTimeout(resolve, 100)) // 100ms delay
}
}
| Pattern | Budget consumption | Recommendation |
|---|---|---|
| Simple chain (A to B to C) | Low | Generally safe |
| Fan-out (A to B, C, D, E) | Moderate | Limit concurrency |
| Deep recursion (A to A to A...) | High | Set max depth |
| Unbounded loops | Very high | Avoid, use queues |
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.