Functions reference
Files and names
Section titled “Files and names”| Rule | Value |
|---|---|
| Directory | functions/ at the root of the project. If the project sets a root directory, inside that |
| Entry points | Every .js or .ts file directly inside functions/. One file is one function |
| Name | The file name without its extension. Lowercase letters, digits and hyphens; starts and ends with a letter or digit; at most 128 characters |
| Subdirectories | Shared code. Never deployed on their own, bundled into whatever imports them |
| Imports | Relative imports and npm packages your build installed are bundled with esbuild into one file per function. Node built-ins stay external |
| Build step | None. TypeScript types are stripped, not checked |
| When | Production deploys only: a push to the production branch, or a promotion. Preview builds skip the directory |
| Bundle size | 900 KB per function. Over that the build fails |
| Removal | Deleting the file and deploying removes the function. Rolling back to an earlier deployment restores the functions that deployment had |
A file whose name breaks the rule fails the build with [functions] Build failed: invalid function name, and so does a .js and a .ts file sharing one name. Nothing is deployed from a failed build, so the previous deployment, functions included, stays live.
Handler
Section titled “Handler”export default async function handler(request: Request, context: Context): Promise<Response>request is a Web API Request. The return value is a Response, or a promise of one. Both are Node 22 natives, as are Headers, ReadableStream, AbortSignal and fetch; there are no polyfills and no deploybase-specific types.
Four export shapes are accepted, resolved in this order. We recommend the first.
| Shape | Example |
|---|---|
| Default export function | export default function handler(request, context) {} |
Default export object with fetch |
export default { fetch(request, context) {} } |
Named export fetch |
export function fetch(request, context) {} |
| CommonJS assignment | module.exports = function (request, context) {} |
request.url is the function’s own public URL, https://{name}-{subdomain}.fn.deploybase.eu/, plus whatever query string the caller sent, on both doors. Headers arrive lower-cased; host is the function’s hostname.
context has six members:
| Member | What it does |
|---|---|
context.env |
The project’s runtime environment variables, as a frozen object of strings. See environment |
context.waitUntil(promise) |
Lets work outlive the response on the HTTPS door: the reply is sent and the promise keeps running, with a rejection logged rather than thrown. On a queued call it does nothing, because the container exits when the handler returns. Work that must complete belongs before the return |
context.trigger |
Which door ran the call: "http" on the HTTPS door, "queued" on a queued call, "schedule" on a scheduled run, including a run-now call (POST …/schedule/run). Set by the platform, never from a request header |
context.scheduledFor |
On a scheduled run, a Date: the cron slot it was started for, without jitter. On a run-now call, the moment it was requested. null on the other doors |
context.manual |
true on a run-now call (POST …/schedule/run), false everywhere else |
context.clientAddress |
The caller’s IP address on the HTTPS door, as a string. null on queued calls and scheduled runs. See client address |
Streaming response bodies work on the HTTPS door, including text/event-stream, and multiple Set-Cookie headers are preserved. On a queued call the body is read in full before it is stored, and Set-Cookie headers are joined into one.
Runtime: Node 22 on both doors.
Client address
Section titled “Client address”context.clientAddress is the IP address the request came from, for rate limiting or blocking abusive callers. Use it rather than reading headers yourself: the platform sets it, and a caller cannot change it by sending headers of their own.
export default async function handler(request, context) { if (await tooManyRequests(context.clientAddress)) { return new Response("Slow down", { status: 429 }); } return new Response("Hello");}| Door | context.clientAddress |
|---|---|
| HTTPS | The caller’s address, for example "203.0.113.7" |
| Queued call | null. A queued request is stored for 7 days, so the caller’s address is left out of it |
| Scheduled run | null. There is no caller |
The same address arrives in the x-real-ip header and as the first entry of x-forwarded-for. A value a caller puts in x-real-ip or x-forwarded-for is replaced before your function sees it, and a Forwarded header is removed. The later entries of x-forwarded-for are internal addresses; do not use them.
fn.deploybase.eu has no IPv6 address yet, so callers reach your function over IPv4 and the address is an IPv4 one. Once IPv6 is added, one visitor can use any address in an IPv6 /64, so count a rate limit per /64 rather than per address.
A function last deployed to production before this field existed has context.clientAddress undefined on a queued call until its next production deploy. Treat a missing value like null.
Keeping as little as you need
Section titled “Keeping as little as you need”An IP address is personal data under the GDPR. When your function stores it, your team is the controller for that data and decides why it is kept and for how long. These keep the footprint small:
- Store a keyed hash, not the address. A rate-limit counter only needs to tell callers apart, so key it on an HMAC of the address with a secret runtime variable. A plain SHA-256 is not enough: every IPv4 address can be hashed in minutes.
- Expire what you store as soon as the window it serves is over, such as a minute for a rate limit.
- Shorten addresses you keep for statistics, for example an IPv4 address to its first two parts (
203.0.x.x), which is what deploybase forms store by default. - Leave it out of
consoleoutput. What your function logs is kept for 7 days (see errors your callers see), and the console output of a queued call is readable by everyone on your team with the Member role or above.
import { createHmac } from "node:crypto";
function callerKey(context) { if (!context.clientAddress) return null; return createHmac("sha256", context.env.RATE_LIMIT_SECRET) .update(context.clientAddress) .digest("hex");}On our side, the function runtime hands the address to your function and does not write it to its own per-request log line or to stored queued calls. Two places do see it:
- Our ingress access log records every request with its IP address, for security and abuse handling, and keeps it for 7 days.
- The queued endpoints’ rate limiter counts callers by a keyed hash of their address, never the address itself, and keeps that hash for up to 2 hours.
Our DPA covers the processing we do for you.
Hostname
Section titled “Hostname”| Item | Value |
|---|---|
| Pattern | https://{name}-{subdomain}.fn.deploybase.eu |
{subdomain} |
The project’s subdomain, the first label of {subdomain}.sites.deploybase.eu. Shown under Configure → General |
| TLS | Automatic. Plain HTTP redirects to HTTPS |
| Length | {name}-{subdomain} together must fit in 63 characters, the DNS label limit. A longer combination is refused at deploy time with a line in the build log |
| Unknown hostname | 404 |
| Custom domains | Not available. A function answers at its own hostname only; there is no way to route a path on your site’s domain to it. The exception is Astro on-demand rendering: its routes answer on the site’s own domains, custom domains included |
Every request goes through the component that scales instances. It adds an x-keda-http-cold-start header to the reply, true when the call waited for an instance to start.
Limits
Section titled “Limits”One row per limit, per plan. The Free plan has no HTTP functions; its functions can still be queued.
| Limit | Starter | Pro | Scale |
|---|---|---|---|
| Memory per instance | 512 MB | 512 MB | 1 GB |
| Concurrent requests per instance, then another instance starts | 5 | 10 | 25 |
| Instances per function | Up to 3 | Up to 3 | Up to 3 |
| Always-warm functions per team | 1 | 2 | 5 |
| Request budget | 60 seconds | 60 seconds | 60 seconds |
| Schedules | Every 15 minutes, 5 per team | Every minute, 25 per team | Every minute, 100 per team |
Each paid plan keeps that many of your team’s production functions always warm: one instance stays running with no traffic at all, so they do not wait for a cold start. It is included in the plan price. The functions are picked automatically, and there is no setting for it yet. The server of your live deployment goes first, then the HTTP functions deployed most recently, so a production deploy can move a slot to the function it just deployed. A plan change moves them within minutes, without a redeploy. A branch preview, or an older deployment that is no longer live, is never kept warm.
Every other function scales to zero when it goes quiet. A production function, which includes every HTTP function and the server of your live deployment, sleeps after 30 minutes without traffic. The server of a branch preview, or of an older deployment that is no longer live, sleeps after about 2 minutes. During the beta, the number of production functions that can stay up for 30 minutes is limited across the platform; when the limit is reached, the functions deployed longest ago sleep after about 2 minutes instead. Always-warm functions do not count against that limit. The next request after a function sleeps waits for a cold start, listed below.
A team let into the earlier closed beta by hand keeps HTTP functions on any plan, Free included, at 512 MB and 10 concurrent requests per instance, or its plan’s own values where those are higher. Its schedule limits stay those of its plan.
These hold on every plan:
| Limit | Value |
|---|---|
| Request body, HTTPS door | 10 MiB, then 413 |
| Request body, queued door | 1 MB |
| Cold start | 2 to 5 seconds for a small HTTP function. An Astro server also downloads and loads its bundle first: about 7.5 seconds (7.4 to 7.7 over five cold starts of a small Astro demo, 10 October 2026). A start that also has to bring up a node takes longer |
| Requests per month | Not counted during the beta. Fair use; abusive traffic may be throttled |
| Outbound network | HTTPS to the public internet. Port 80, private ranges and the cloud metadata address are blocked |
The request budget is not configurable during the beta.
An Astro server’s deadline is 50 seconds; see what does not work yet.
Timeouts
Section titled “Timeouts”The 60-second budget bounds what the caller sees, not what your code does. When it expires:
- If nothing has been sent yet, the caller gets a
504. - If the response had started, the connection is cut mid-body.
Either way the handler keeps running until it finishes or the instance is replaced, because JavaScript has no way to cancel a running promise from outside. Side effects after the deadline still happen: a database write, an email, a third-party call. Do not rely on a timeout to cancel work; bound it inside the handler with an AbortSignal or a deadline of your own.
On a queued call the same budget applies. The container is stopped when it expires, the invocation ends as timed_out, and the poll reply carries the timeout message listed under queued calls.
Errors your callers see
Section titled “Errors your callers see”| Status | Cause |
|---|---|
404 |
No function at this hostname: unknown name, unknown subdomain, the team’s plan does not include HTTP functions, or not deployed yet |
413 |
Request body over 10 MiB |
500 |
The handler threw, returned a rejected promise, or returned something that is not a Response |
504 |
The request budget expired before any byte was sent |
No stack trace or error text reaches the caller in any of these. A 500 is logged with the error on our side; the dashboard does not show HTTPS-call logs yet, so to read the error reproduce the call through the queued door.
What your function writes to console during an HTTPS call, and our own line for each request (method, path, status and duration, never the query string), are kept in our log store in the EU for 7 days. We use them to operate the platform and to answer support requests, and they are deleted after 7 days. Do not log secrets or personal data you would not want kept for that long.
Queued calls
Section titled “Queued calls”The queued door runs the same file in a fresh container per call. Both endpoints are public and need no authentication. The API reference holds the full schemas: invoke and poll.
| Endpoint | Status | Reply |
|---|---|---|
POST /api/v1/public/functions/{subdomain}/{name} |
202 |
Body {"data": {"invocation_id", "status": "pending", "poll_url"}} |
GET /api/v1/public/functions/{subdomain}/{name}/invocations/{id} |
200 |
Body {"data": {"id", "status", "started_at", "finished_at", "response_status", "response_headers", "response_body", "error_message"}} |
Base URL https://api.deploybase.eu. The poll_url is relative to it. The method, query string, headers and body of the POST are what the handler receives as its Request.
| Rule | Value |
|---|---|
| Statuses | pending, running, then one of completed, failed, timed_out |
| Result fields | response_status, response_headers and response_body (base64) appear once the status is terminal. A failed call whose handler threw still carries the handler’s own 500 response |
error_message |
Present on failed and timed_out. One of the sentences below, never the error itself |
| Request body | 1 MB. A larger body is cut at 1 MB today rather than refused; this will become a 413 |
| Rate limit | 20 requests per minute per IP address on each endpoint, counted across all functions, then 429 |
| In flight | 10 queued calls per team at once, then 429 with Too many concurrent invocations |
| Memory | 512 MB |
| Retention | Request, result and console output are kept for 7 days, then deleted |
| Access | Not gated by plan. Any team’s functions can be queued, Free included |
The error_message sentences, exactly as they appear:
| Ended as | error_message |
|---|---|
timed_out |
The function did not finish within its timeout of 60s. |
failed |
The function ran out of memory. |
failed |
The function returned an error. Check the invocation logs. |
failed |
The function did not return a valid response. |
failed |
This function is not deployed. Redeploy the project and try again. |
failed |
The function could not be run. This is a platform issue; please retry. |
Two more appear only when a call is reclaimed after a platform restart: The function did not finish before its timeout. and The function was never started. This is a platform issue; please retry.
Everything a queued call wrote to console.log and console.error is shown on the function’s page in the dashboard under Runs, for the Member role and above (a Viewer sees a short note instead); it is never part of the public poll reply. Logs for HTTPS calls are not visible yet.
Schedules
Section titled “Schedules”A function runs on a timetable when its file exports a cron expression. A scheduled run goes through the queued door: a fresh container, the same queued-call limits, and a record under Runs.
export const schedule = "0 * * * *";
export default async function handler(request: Request, context: Context) { await refreshExchangeRates(context.scheduledFor); return new Response("ok");}Declaration
Section titled “Declaration”| Rule | Value |
|---|---|
| Form | One line, starting at column 0: export const schedule = "…"; with a single- or double-quoted string. The semicolon and a trailing // comment are optional |
| Read how | From the source text at build time. The module is never executed to find it |
| Refused | Anything computed: a template literal, a concatenation, a variable, export let or export var, an export list such as export { schedule }, or a second declaration |
| Build error | [functions] Error in functions/report.js, then [functions] Build failed: schedule must be a plain string, for example export const schedule = "0 * * * *". An invalid expression fails the build the same way, with a sentence naming the problem |
| Applied | Production deploys and promotions only. A preview build never registers a schedule |
| Rollback | Promoting an older deployment restores the schedule that deployment declared, or removes it if it had none |
| Removal | Delete the line and deploy. The function stays live without a timetable |
| Never fires | A valid expression with no future run, such as 0 0 31 2 *, is not scheduled. The build log says so; the build does not fail |
Expression
Section titled “Expression”| Rule | Value |
|---|---|
| Fields | Exactly five: minute, hour, day of month, month, day of week. crontab(5) has the syntax: lists, ranges, steps, month and weekday names |
| Time zone | UTC, always. There is no per-function time zone |
| Descriptors | @hourly, @daily, @midnight, @weekly, @monthly, @yearly, @annually |
| Length | 100 characters |
| Refused | A sixth (seconds) field, TZ= or CRON_TZ= prefixes, @every and every other @ form, and the extensions L, W, # and H |
Plan limits
Section titled “Plan limits”| Plan | Shortest interval | Active schedules per team |
|---|---|---|
| Free | 1 hour | 2 |
| Starter | 15 minutes | 5 |
| Pro | 1 minute | 25 |
| Scale | 1 minute | 100 |
- Interval is measured over the next 50 runs, not read from the minute field, so
0,1 * * * *counts as every minute. - Over a limit never fails the deploy. The schedule is paused, the build log says which limit, and the function stays live and callable. When a team has more schedules than its plan allows, the oldest keep running.
- Lifted on its own. After an upgrade, a slower cron or a removed schedule, a plan-paused schedule resumes at the next deploy or within the hour, and its first run is the next slot after that.
Jitter
Section titled “Jitter”Each function fires a fixed number of seconds after its slot, so functions sharing 0 * * * * do not all start in the same second.
| Rule | Value |
|---|---|
| Offset | Up to a tenth of the shortest interval, at most 300 seconds |
| Stability | Derived from the function’s id: the same offset on every run and every deploy |
context.scheduledFor |
The slot itself, without the offset |
Delivery
Section titled “Delivery”- At most one run per slot. Two copies of one slot never start, whatever happens on our side.
- No automatic retries. A failed or timed-out run is not repeated. Make the handler idempotent, keyed on
context.scheduledFor, so a run you start again with a run-now call (POST …/schedule/run) does no harm. - Redeploying the same cron keeps the timetable. Changing the cron recomputes it.
A slot that does not run is skipped and counted, never queued for later:
| Skipped when | What happens next |
|---|---|
| The previous scheduled run of the function, or a run-now call, is still pending or running | The next slot runs as usual |
| The slot is later than one interval, capped at one hour, because the platform was down or busy | The slot is skipped. The newest slot still inside that window runs at once; when there is none, the schedule moves on to the next slot after now |
| The team already has 10 queued calls in flight, scheduled runs included | The next slot runs as usual |
| The team is suspended | Nothing runs until the suspension ends |
Request
Section titled “Request”A scheduled run receives a POST to the function’s own URL with no query string.
| Part | Value |
|---|---|
request.url |
https://{name}-{subdomain}.fn.deploybase.eu/ |
content-type |
application/json |
| Body | {"scheduled_for": "2026-10-02T13:00:00Z", "schedule": "0 * * * *"}: the slot in RFC 3339 UTC, and the expression |
On a run-now call, scheduled_for is the moment it was requested.
Auto-pause
Section titled “Auto-pause”A schedule pauses itself when its last 10 finished scheduled runs all failed. A failure is a run that ended failed or timed_out, or completed with a status of 500 or above; a 4xx answer is the handler’s own and breaks the streak, and so does a cancelled run. Only runs since the function’s code last changed, or since the last resume, count. Runs started with a run-now call count too.
On the pause the team gets one email naming the function and the last error, unless the team turned it off by setting function_schedule_paused to false with PUT /api/v1/notification-preferences. The function’s URL and queued calls keep working; only the timetable stops.
There are three ways back:
- Deploy changed code. A production deploy that changes the function’s bundle, a changed import included, resumes the schedule and starts the count again. A deploy of identical code does not.
- Resume on the function’s page in the dashboard, or over the API below, once the cause is gone. The page also has Copy for your AI, which copies the error, the function’s settings and the last failed run’s log for an assistant.
- Remove the
export const scheduleline and deploy, if the function should no longer run on a timetable.
Resume and Run now
Section titled “Resume and Run now”The function’s page in the dashboard has a Run now button on its Schedule panel, and Resume on a schedule paused by failures. Both need the member role or above in the team. A viewer can read schedules but not act on them.
| Endpoint | Reply |
|---|---|
POST /api/v1/projects/{id}/functions/{functionId}/schedule/resume |
200 with the schedule. Lifts an auto-pause: the count starts again and the next run is the next slot from now. A schedule that is not paused is returned unchanged. The plan’s limits are checked straight after: a schedule the plan no longer allows comes back paused with pause_reason plan_limit |
POST /api/v1/projects/{id}/functions/{functionId}/schedule/run |
202 with {"data": {"invocation_id": "…"}}. Starts one run now, allowed while paused. The timetable does not move |
| Status | Code | Cause |
|---|---|---|
403 |
ACCOUNT_SUSPENDED |
Run now while the team is suspended |
404 |
RESOURCE_NOT_FOUND |
No such function in this project, or it has no active schedule |
409 |
SCHEDULE_PAUSED_BY_PLAN |
Resume of a schedule the plan’s limits paused. It resumes on its own once the plan allows it |
409 |
SCHEDULE_RUN_OVERLAP |
Run now while a scheduled run of the function is still pending or running |
429 |
RATE_LIMIT_EXCEEDED |
Run now with 10 queued calls of the team in flight |
GET /api/v1/projects/{id}/functions/{functionId} returns the schedule under schedule: the next three fire times, the pause state, consecutive_failures, the last run and skip, and the plan’s limits. It is null for a function without one. On GET …/invocations, each row carries trigger and scheduled_for, and ?trigger=schedule lists scheduled runs, run-now calls included, ?trigger=schedule_manual only the latter, and ?trigger=queued only queued calls.
Environment
Section titled “Environment”A function reads the project’s environment variables that are set to reach it. Changing one does not need a deploy.
Which variables a function gets
Section titled “Which variables a function gets”| Rule | Value |
|---|---|
| Target | Runtime ticked in the dashboard, or "runtime" in the variable’s targets over the API. A variable with only Build ticked never reaches a function |
| Scope | production or all. HTTP functions run in production only, so a preview variable never reaches one. An Astro server built for a branch preview gets the preview and all variables instead |
Same key in production and all |
The production value wins |
| Which functions | Every function of the project, on both doors |
| Rollback or promotion | Changes the code, never the variables. Variables are not part of a deployment, so an older deployment’s functions run with the values set today |
context.env and process.env
Section titled “context.env and process.env”| Object | Holds |
|---|---|
context.env |
Exactly the project’s runtime variables and nothing else. Frozen; a key you did not set reads undefined |
process.env |
The same variables, plus whatever the runtime and the platform set: PATH, HOME, NODE_ENV, the FN_ variables and others |
context.env is the contract. What process.env holds besides your variables can change without notice.
export default async function handler(request: Request, context: Context) { const balance = await fetch("https://api.stripe.com/v1/balance", { headers: { authorization: `Bearer ${context.env.STRIPE_SECRET_KEY}` }, }); return new Response(balance.ok ? "ok" : "upstream error", { status: balance.ok ? 200 : 502 });}Never log a value from context.env or put one in a response. Console output of queued calls is shown to everyone on the team with the Member role or above, and a queued call’s poll reply is public to anyone holding its URL.
When a change applies
Section titled “When a change applies”| Function state | When it sees the change |
|---|---|
| Warm HTTPS instance | Replaced right after the save by an instance started with the new values. Requests during the switch can still reach the old instance |
| Scaled to zero | Starts with the new values on its next request. Nothing is started early |
| Queued call | Every call that starts after the change is applied has the new values |
| Anything the first attempt missed | A pass every 10 minutes applies the current values to every function |
The project’s Environment page shows the progress after a save: Applying to functions: 1 of 3, then Applied to 3 functions. It follows for about a minute; if some functions are still pending by then, it says the rest pick it up within 10 minutes. Over the API, GET /api/v1/projects/{id}/functions/env/status lists the runtime keys, never the values, and whether each function runs with the current set.
context.env is read once when an instance starts, which is why a warm instance is replaced rather than updated.
Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Runtime total | 64 KB: every key and value a function receives, added up after the production-over-all rule. A write that would go over is refused with 422. If the set is over anyway, a deploy leaves functions on their previous values and says so in the build log |
| Per variable | Size, count and key format are in the environment variables rules, with the names reserved for the platform |
NODE_OPTIONS |
Accepted with a warning. In a function it changes how Node.js starts, so a wrong value stops every function in the project |
Known issue on the queued door
Section titled “Known issue on the queued door”A function last deployed to production before runtime variables shipped builds context.env with an older rule on the queued door: it leaves out names containing _PORT or _SERVICE_ or starting with NODE_, and can include variables of the container image. process.env has every variable either way. One production deploy fixes it. The HTTPS door is not affected.
| Data | Where and for how long |
|---|---|
| Queued calls | The request headers and body, the result and the console output are stored in Paris for 7 days |
| HTTPS calls | No request or response body is stored. Console output and one line per request (method, path, status, duration) are kept in our log store for 7 days, and our ingress access log keeps each request’s IP address for 7 days |
| Bundles | Stored in a private storage zone in Germany, deleted with the deployment |
| Client addresses | Handed to your function on the HTTPS door as context.clientAddress. The queued endpoints are rate limited per IP address, counted under a keyed hash of the address that is kept for up to 2 hours. The address itself is never stored with a queued call |
Our DPA covers personal data your functions process.
Live 022265e7 built