Functions
Functions are serverless functions for your site: a light backend for the few things a static page cannot do on its own — receive a webhook, call an API with a key the browser never sees, handle a form your way. They run per request on the edge that serves your pages, so there is no server to keep running. They keep no data of their own: there is no database beside them, and nothing runs on a schedule or in the background.
Put a folder named _functions at the top level of what you publish. Each JavaScript file in it answers
the path it is named after, on your site's own address:
Code
Code
Functions publish with the rest of the folder, in the same version: rolling back to an earlier version
brings back its functions too, and a version's pages and its code never disagree. Your code itself is
never served — /_functions/api/hello.js answers 404.
Functions run only on the plans that include them — Pricing lists which, and
when no plan does yet, it says nothing about them. On a plan without them, a folder with _functions still publishes:
the files in it are left out of what is served, the publish says so with a warning, and the paths answer
404 until you publish again on a plan that has them.
How a file becomes a route
The path of a file under _functions, without its extension, is the route.
| File | Answers |
|---|---|
api/hello.js | /api/hello |
api/index.js | /api |
api/users/[id].js | /api/users/ and one more segment, as params.id |
api/files/[[path]].js | /api/files and everything below it, as params.path, an array of segments |
- A file or folder starting with
_is a helper: it can be imported by a route, and answers nothing itself. Names starting with__are reserved. - Names use letters, digits,
.,_and-; a parameter's name starts with a letter. - The files the site serves are
.jsor.mjsmodules. TypeScript works when you publish with the CLI, which compiles it (see below). - When two routes could both match a path, the more specific one wins: a fixed segment before
[id],[id]before[[path]]. - A route wins over a file and over a
_redirectsrule at the same path. A proxy rule in_redirects(see Calling your own API) may not share a path with a route: the publish is refused and names both. - Paths under
/__d2r/belong to the platform and never reach a function.
A version may hold up to 20 routes and 100 files under _functions, 5 MB in all.
What a function receives
Export one function per method — onRequestGet, onRequestPost, onRequestPut, onRequestPatch,
onRequestDelete, onRequestHead, onRequestOptions — or onRequest for every method. A method with no
handler answers 405 with an Allow header. Each is called with one object:
| Field | What it is |
|---|---|
request | The visitor's Request |
env | The site's variables, each a string |
params | What the route's [name] and [[name]] segments matched |
clientIp | The visitor's address — an IPv6 address cut to its /64 |
waitUntil(promise) | Keeps work going after the answer is sent, such as a log write |
It returns a Response. The code is a standard ES module running on Cloudflare Workers, so fetch,
crypto.subtle, URL, TextEncoder, Response.json and the other web APIs are there. Node's built-in
modules (node:fs, node:crypto, …) are not.
The request is rebuilt on the way in, so your code never sees the platform's own cookies, Cloudflare's
cf object, or headers that name the visitor's network (cf-connecting-ip, x-forwarded-for and the
like) — clientIp is the one thing it knows about who called.
Keys and settings
A function reads its keys from env, never from the files you publish. Set them in the site's
Functions tab, with the CLI, or with the MCP server's update_site:
Code
- A value is never shown again. It is encrypted when you save it; the dashboard and
drop2run infoshow its name and last four characters. - A change needs no new publish. It reaches every route within about a minute. Code that copies
envinto a variable at the top of a module keeps the old value until that copy is replaced, so readenvinside the handler. - Names are uppercase letters, digits and
_, starting with a letter or_. A site holds up to 50 variables, each up to 5 KB, 40 KB in all. - Removing a variable always works, on any plan — a key can always be taken back.
npm packages and TypeScript
What you drop in the dashboard is published as it is, and the platform has no build step: a route that
imports an npm package by name (import Stripe from "stripe") fails when it first loads. Imports between
your own files under _functions work either way.
drop2run deploy bundles each route for you: one module per route with everything it imports — your
helpers, npm packages from the folder's node_modules, TypeScript compiled to JavaScript. Run
npm install in the folder first. It picks a package's Worker or browser build, since Node's built-ins
are not available, and says which file and line it could not bundle if one fails. --no-bundle publishes
_functions as it is.
What a response may carry
Your function sets its own status, body and headers, including CORS headers and Content-Type. A few are
changed on the way out, on every answer:
Cache-Controlis alwaysno-store: an answer from a function is never cached by the edge or by the browser, so a fix you publish is the next answer.X-Content-Type-Options: nosniffand the site'sX-Robots-Tagare always added.- Headers that would act beyond your site are dropped:
Clear-Site-Data,WWW-Authenticate,Proxy-Authenticate,Origin-Agent-Cluster,NEL,Report-To,Reporting-Endpoints,Set-Login,Alt-Svc, and anything startingcf-orx-d2r-. - A
Set-Cookiekeeps its name, value,Path,Expires,Max-Age,Secure,HttpOnly,SameSiteandPartitioned.Domainis always dropped, so a cookie belongs to the exact host it was set on. Name a session cookie with the__Host-prefix: on*.dropto.live, another site cannot overwrite it. - An answer may be up to 25 MB; a longer one is cut off.
Who can call a function
Anybody who can open the site. A route on a public site answers every visitor and every script, not only your pages, so check what a request asks for before acting on it.
- Password and invite-only come first. On a site behind a password or invite-only, a call without the visitor's access answers as any page would. A webhook from a service outside cannot sign in, so put routes that receive webhooks on a public site.
- Pages of other sites cannot call in with your visitors' cookies. On
*.dropto.live, a call made from a page of anotherdropto.livesite is refused with 403. On a site behind a password or invite-only, every call from a page of another site is refused — including the browser's return from a sign-in provider — so a route that receives one belongs on a public site too. On your own domain, calls between your own subdomains work. - Calls with no browser behind them — a webhook, a server,
curl— have neither header and go through.
Calling other services
fetch reaches the public internet over http and https, on ports 80 and 443. To keep one site from
turning the platform on another, a function cannot reach:
- private, loopback and other non-public addresses, written as numbers in any form;
localhost,dropto.run,dropto.liveand their subdomains — another site on the platform included — and Cloudflare'sworkers.dev,r2.devandcfargotunnel.com;- raw TCP sockets.
Redirects are not followed for you: a 3xx comes back to your code, and following it is another fetch.
A call to a function may make up to 10 fetch calls.
Limits and errors
| Limit | |
|---|---|
| CPU time per call | 50 ms unless your plan says otherwise — the time your code computes, not the time it waits on fetch |
| Request body | 10 MB |
| Response body | 25 MB |
| Waiting for the first byte | 30 seconds |
| Silence between two chunks | 60 seconds |
| A whole call | 5 minutes |
fetch calls per call | 10 |
Calls count against your plan's monthly allowance, every site of the account together. Past it, every
function of the account answers 429 until the month turns; the pages keep serving. Anybody who can reach
a route can spend that allowance, so a public route that does little should still do little for an
unexpected caller. A burst on one site is also cut off by a per-site limit, which answers 429 with
Retry-After.
When something goes wrong, the answer is JSON naming a fixed code and never your code's message, which could hold a key:
Code
| Status | error | Means |
|---|---|---|
| 404 | function_not_found | No route answers this path in the live version |
| 405 | method_not_allowed | The route exports no handler for this method |
| 413 | request_too_large | The request body is over 10 MB |
| 429 | rate_limited, functions_suspended | Too many calls in a short time, or the month's allowance is spent |
| 500 | function_error | Your code threw, failed to load, ran out of CPU time, or did not return a Response |
| 502 | function_bad_response | The Response had a status below 200, or opened a WebSocket |
| 502 | functions_unavailable | The platform could not reach the functions service; try again |
| 504 | function_timeout | No answer within the times above |
There is no log of your code's console.log yet. To see what a route does, return it in the answer while
you work on it.
Previews and rollbacks
Functions run only on the site's live address and its custom domains. A preview link of another version never runs functions — its function paths answer 404 — so test a change by publishing it, and roll back from Versions if it is wrong. Rolling back changes the routes with the pages.