Skip to content

Functions reference

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.

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.

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.

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 console output. 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.

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.

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.

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.

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.

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.

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");
}
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
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 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.

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
  • 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

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.

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 schedule line and deploy, if the function should no longer run on a timetable.

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.

A function reads the project’s environment variables that are set to reach it. Changing one does not need a deploy.

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
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.

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.

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

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