Skip to content

Security basics

"Edge Function returned a non-2xx status code" in Supabase

"Edge Function returned a non-2xx status code" means your Supabase function answered with an error. Here is where to read the real one.

Vlad Tkachenko11 min read
An envelope sent back with a stamp in amber across its face and its flap open, showing the folded letter still inside.

In short

  • "Edge Function returned a non-2xx status code" is what supabase-js says about any reply from your function outside the 200s. The real reason came back in the same reply, and the function's logs hold the rest.
  • Read the status number first. 401 means the request was refused before your code ran, 404 that no function by that name is deployed, 500 that your code failed, 546 or 504 that it ran out of resources or time.
  • Two quick fixes make the message stop and open a hole: calling the API from your frontend with the key in it, and switching off JWT verification with no check of your own in its place.

You press the button in your app that calls an Edge Function, and instead of a result your app shows this:

Edge Function returned a non-2xx status code

It is the same sentence whatever went wrong. A missing key, a function that crashed, a request turned away before your code ran: all of them read "Edge Function returned a non-2xx status code", so the message on its own tells you almost nothing about what to fix.

What it does not tell you is that the reason has already arrived. The error came back in the same reply as that sentence, and supabase-js does not read it out to you. Searching the sentence gets you other people's guesses. Reading the reply gets you your own answer.

What does "Edge Function returned a non-2xx status code" mean?

Your function answered, and the answer was an error. That is all the sentence says.

Every reply on the web carries a status code, a three-digit number saying how the request went. Anything in the 200s means it worked. When your app calls a function through supabase.functions.invoke, supabase-js looks at that number, and if it is outside the 200s it hands your app a fixed sentence in place of the data. The sentence is written into supabase-js itself, word for word, and the error it comes with is called a FunctionsHttpError.

Think of the call as a letter you post to a back office. The office either answers what you asked or sends the letter back. When it comes back, supabase-js reads the stamp on the envelope and tells you it was returned. It does not open the envelope. The note inside says why, and the office kept a logbook of everything that happened while your letter was there.

Two close relatives look similar and mean something else. Failed to send a request to the Edge Function means no readable reply came back at all, and in a browser that is often a missing CORS header, which the post on that message covers. And if several unrelated parts of your app broke at the same moment, check whether it is Supabase or your app before you open any code.

The reply carries the status and the reason. supabase-js passes your app one sentence, and the reason stays attached to the error and in the function's log.

Where is the real error?

In two places, and you can read both without changing a line of code.

The reply itself. Open your live app in Chrome, Edge or Firefox, press F12, choose the Network tab and repeat whatever failed. Click the request whose address contains /functions/v1/ followed by your function's name. Its Status column has the number, and its Response tab has whatever the function sent back with it: a message your own code wrote, a short JSON body from Supabase's platform, or the words Internal Server Error.

If you, or the tool that writes your code, can change the call, Supabase's error handling guide shows how to read the same reply inside the app, because the original response travels with the error as error.context:

const { data, error } = await supabase.functions.invoke('your-function', { body })
if (error) console.log(error.context.status, await error.context.text())

The function's logbook. In the Supabase dashboard, open Edge Functions, pick the function, and read two tabs. Invocations lists every request with its status code and how long it took. Logs holds what Supabase's logging guide calls platform events, uncaught exceptions and custom log messages, which means anything your code printed with console.log or console.error. Reproduce the failure once, note the time, and read both tabs at that minute.

Read the status code first

The number tells you how far your request got before it was turned back, and that decides where the reason is written down.

StatusWhat happenedWhere the reason is
401Refused before your code ran, or your code refused itThe reply. Invalid JWT or Missing authorization header is the platform
404No function with that name is deployed in this projectThe reply reads Requested function was not found
500Your code ran and threw an error, or returned a 500 on purposeThe Logs tab
503The function failed to start at allThe Logs tab: an import, a syntax error or the function's configuration
504No reply within 150 secondsThe Invocations tab, then whatever your code was waiting on
546It ran out of CPU time or memoryThe Logs tab says which
Any other 4xxYour own code chose it, for example by passing on a service's answerThe reply

The meanings come from Supabase's status code reference and its limits page. A 404 needs no more than this table: check the function's name in your app against the list in the dashboard, and check it is deployed to the project your app talks to. The other rows have a section each below.

500: your function ran and failed

Something in your code threw an error, and the Logs tab has the line.

Supabase's page on this error splits it in two. If the reply says exactly Internal Server Error, your code crashed and the log shows the type of error and the line it happened on, such as TypeError: Cannot read properties of undefined. If the reply says anything else, your code wrote that message itself and returned the 500 on purpose, so search the function for the status it sent.

One cause is worth ruling out before any other in a function that calls OpenAI, Stripe or any paid API: a secret that exists on your machine and not on your live project.

Inside the function, a secret is read with Deno.env.get('OPENAI_API_KEY'). When no secret by that name is set, Deno's documentation says this returns undefined instead of failing. So the function carries on, builds a request with the word undefined where the key should be, and sends it. OpenAI answers with its own 401, "Incorrect API key provided", and your function either passes that on or falls over trying to read a reply it did not expect. In the envelope picture, the office opened your letter, went to the drawer where the key should be, found it empty, and sent back a note about a wrong key.

The repair is on the Edge Function Secrets page of your Supabase project. Supabase's secrets guide says a function reads a new secret immediately, so you do not redeploy, and that no secret name may start with SUPABASE_, because that prefix is reserved for the values Supabase sets for you. A secret you set for local development stays on your machine; the live project has its own list.

401: refused before your code ran

If the reply says Invalid JWT or Missing authorization header, your function never saw the request. Supabase's platform turned it away at the door.

That door is a check called verify_jwt, on by default, which reads the Authorization header of every request before your code runs. Supabase's page on the 401 lists three ways to fail it:

  • The key does not match your project's keys. For example a key copied from another project, or an old one still set somewhere after a rotation.
  • There is no Authorization header at all. supabase-js adds one for you. A plain fetch to the function's address, or a webhook from another service, does not.
  • Your project uses Supabase's newer signing keys. The built-in check only understands the older format, and Supabase's fix is to switch the check off for that function.

That last point goes further than one case. Supabase now recommends turning the built-in check off and managing authentication directly in your function code. The recommendation has two halves, and the second is the one that gets dropped.

If the 401 came with a message your own code wrote, the platform let it through and your function refused it, or passed on a 401 from the service it called, which is often the empty secret from the 500 section. This is also a different check from the one behind PGRST301 on your database queries, which has a page of its own. If the function's log shows invalid claim: missing sub claim, it asked Supabase Auth who was calling and was handed your project's key instead of a signed-in user, which that message's own post traces.

The status says how far the request got. A 401 at the gate never reached your code; a 401 passed back from the service it called did.

546 and 504: out of resources or out of time

Supabase stops a function that uses too much, and the two numbers say which limit it hit.

546 is compute. The limits page gives a function 256 MB of memory and each request 2 seconds of CPU time. Go over either and the reply reads Function failed due to not having enough compute resources (please check logs), with the code WORKER_RESOURCE_LIMIT (older logs say WORKER_LIMIT). The Logs tab says CPU Time exceeded or Memory limit exceeded. Supabase's page on the 546 gives image processing, AI embeddings and web scraping as its examples, and its answer is to do less per request or hand the heavy part to another service.

504 is time. A function that sends no reply within 150 seconds gets a 504. The CPU limit counts only the time your code is actually computing: the limits page says it does not include async I/O, which is what waiting on the network is. So a function that sits waiting for a slow AI model is not spending CPU. It is running down the 150-second clock, and when that runs out you get a 504 rather than a 546.

Two fixes that make it worse

Two changes make this message go away within minutes, and both leave you worse off than the error did.

Calling the API from the page instead. The quickest way to stop a failing function from failing is to stop using it: put the key in a VITE_ variable and call OpenAI straight from your frontend. It works on the first try. It also puts the key in the file every visitor downloads, because that prefix is an instruction to compile the value into your bundle, and anyone can read it there. Across the live apps we have scanned, an OpenAI key turned up in the JavaScript of 33, and what a stranger can do with one has a post of its own.

Switching off the check and stopping there. Clearing a 401 by turning off verify_jwt is the first half of Supabase's own advice. Supabase's configuration guide is plain about what it does alone: "it will allow anyone to invoke your Edge Function without a valid JWT". Your function's address is in your frontend for anyone to read. If the function holds an OpenAI key, a stranger can now spend it through you. If it uses the project's secret key, which Supabase places in every function's environment, a stranger's request runs with a key that gets past every row level security rule you wrote.

The second half, a check on who is calling written into the function, is a few lines. Moving a secret into an Edge Function has them, and it explains why even the default check lets more callers through than its name suggests.

You can see the first mistake from outside, which is how we counted it. Our free scan reads your live app's JavaScript the way a stranger would and names every key it finds, including whether it is one that belongs there. It takes about 20 seconds and needs no account: scan your app.

Keeping the key where you put it

The fix you make today can be undone by next week's prompt. A builder asked to make a feature work again is free to take the shortcut above, and your app keeps working while it does.

Reeve Monitor checks every hour so you do not have to:

  • all nine checks every hour, on up to three apps
  • an email the day a re-scan grades your app lower than the one before; a secret key in your JavaScript caps the grade at D on its own
  • whether the app is up, every 60 seconds
  • a monthly report of what it saw

A function running with your secret key gets past every row level security rule. Reeve Care keeps a copy of your Supabase database where nothing in your project can reach it.

  • an encrypted copy on a schedule, kept on Reeve's own storage in the EU, outside your Supabase account
  • each copy verified before it counts, by counting the rows in every table
  • a one-click restore that saves what is there now before it replaces anything
  • your uploaded files as well, once you connect a Storage credential
  • everything Monitor does

What to do now

What to do

  • Reproduce the failure once and note the time.
  • Open the request in your browser's Network tab and read its status and its response before you change anything.
  • Read the function's Invocations and Logs tabs in the Supabase dashboard at that minute.
  • On a 500 from a function that calls a paid API, check the secret is set on the live project's Edge Function Secrets page. No redeploy is needed after.
  • On a 401 from the platform, fix the key or switch the built-in check off, and add a check on who is calling inside the function in the same change.
  • Never clear this error by moving the key into a VITE_ variable.

The check on who is calling is the step that is easiest to skip, and the Edge Function secrets walkthrough covers it in full. The 10-minute security checklist covers what else tends to be left open in a newly launched app.

FAQ

What does "Edge Function returned a non-2xx status code" mean?

It means your Supabase Edge Function replied with an error status, any number outside the 200s, and supabase-js turned that reply into one fixed sentence. The sentence is the same for a crash, a missing key, a request refused before your code ran and a timeout. The reply itself, which says which of those it was, is still attached to the error, and the function keeps a log of its own.

How do I see the real error behind it?

Open your live app, press F12, choose the Network tab and repeat whatever failed. Click the request whose address contains /functions/v1/ and read its status and its response: that is what the function actually sent. Then open Edge Functions in your Supabase dashboard, pick the function, and read the Invocations and Logs tabs at the same time. In code, Supabase's guide reads the same reply with await error.context.json().

Why does my Edge Function work locally but fail when deployed?

Check first whether a secret exists on your machine and not on your live project. The local stack reads its own environment file, and the deployed function reads the secrets set on the project. A secret that is missing comes back as undefined rather than as an error, so the function carries on and fails later, usually when the service it calls refuses an empty key. Set it on the Edge Function Secrets page; Supabase says a new secret is read immediately, with no redeploy.

Should I turn off JWT verification to fix a 401?

Supabase now recommends turning the built-in check off and checking the caller inside your function instead. Both halves matter. Turned off on its own, the check stops nothing, and Supabase's own docs warn that this lets anyone invoke your function without a valid JWT. If the function holds an OpenAI key or the project's secret key, that is anyone on the internet spending your quota or reaching past your row level security rules.

What does error 546 mean on a Supabase Edge Function?

The function ran out of compute: more than 2 seconds of CPU time, or more than its memory allowance, on one request. The reply reads "Function failed due to not having enough compute resources (please check logs)", and the Logs tab says which limit it was, CPU Time exceeded or Memory limit exceeded. Waiting on a slow API does not count as CPU time; a function that waits too long gets a 504 instead.

Written by

Vlad Tkachenko

Founder, Reeve

I spend my time looking at apps built with Lovable, Bolt, v0, Cursor and Replit, and at the short list of mistakes that keep turning up in them.

More about the author

Read next

All articles

Back up your Supabase database before your next change

Reeve keeps an encrypted copy on a schedule and restores it for you if something breaks.

See Supabase backups

Automated external check, not a full audit. Absence of findings is not a guarantee of safety.