Functions best practices
Scope, configure, and instrument a function so it stays inside the CPU, sub-request, and Args ceilings that bound every invocation.
A function runs once for each request that matches the rule pointing at it, and every run is bounded. One ceiling caps the CPU time a run may consume. Another caps the calls it may make out to other services. Most of what keeps a function inside those bounds is decided before the code is written. Three questions settle it: which requests reach the function, what it computes while the request waits, and where its data comes from.
The practices on this page give the reasoning behind each of those decisions. The values themselves belong to Limits, and the symptoms of crossing one belong to Troubleshooting. The sections follow the order in which the decisions arise: the handler pattern, the rule that invokes the function, the two execution ceilings, and the sub-request budget. The last three cover where data comes from, how an instance is configured, and how the output is observed.
ES Modules as the handler pattern
Functions supports two handler patterns, and new code uses ES Modules. A function written this way exports a default object whose fetch method receives the request, the environment variables and bindings, and the execution context:
The signature is what makes the rest of these practices reachable from the handler. Environment variables and bindings arrive in env. The context object carries ctx.waitUntil(promise), which extends the function’s lifetime, and in a firewall handler it carries ctx.deny(), which blocks the request immediately.
The Service Worker pattern registers a listener with addEventListener('fetch', ...) and reads the same values from an event object. Azion maintains it for backward compatibility and recommends migrating functions that still use it. Existing Service Worker code keeps running, so the migration is work to schedule rather than a break to repair. Code that follows neither pattern is not supported. For the before-and-after of each handler, refer to Migrate handler patterns in Functions.
Rules Engine criteria
Creating a function does not run it. A function runs when an incoming request matches a Rules Engine rule whose Run Function behavior selects the function instance. The criteria on that rule decide which requests those are. Criteria that match every path, such as a URI that starts with /, invoke the function on every request the application receives.
A criterion is what attaches the function to a hostname, a path, or any other property of the request. For every variable and comparison operator a criterion can use, refer to Criteria.
Narrowing the criteria to the requests the function has work for changes what the function costs. Functions are billed on two metrics, invocations and compute time, so requests the criteria exclude are requests that are neither invoked nor computed. The pricing page carries both metrics.
The same rule fixes the execution phase. A rule in the Request Phase runs before the response has been processed, so variables describing the content to be delivered are not available to it. A rule in the Response Phase runs as the application delivers content to the user.
Rules and behaviors run in the order they are arranged, and that order has a cost. A behavior that finishes the sequence, such as Deny, stops every rule after it, so a Run Function placed later never executes.
CPU time and wall-clock time
Two ceilings bound a single invocation, and they measure different things. CPU time caps at 2 seconds and counts active computation only. Wall-clock time caps at 5 minutes and includes I/O wait, fetch() calls, and asynchronous operations. A function that crosses the CPU ceiling is terminated. Both ceilings are defaults. To raise one for your plan, contact the technical support team.
The distinction says where the pressure sits. A function that spends its run waiting on an origin stays far from the CPU ceiling, however long the request takes. A function that parses or transforms a large body on every request spends that budget instead.
Work whose result the response does not need is handed to ctx.waitUntil(), which extends the function’s lifetime past the response. That takes the work off the response path without taking it out of the invocation. The 5-minute wall clock still bounds it.
A function runs with no cold start, so a first request after an idle period is not slower for that reason. For the path a request takes to reach the handler, refer to Invocation path.
The sub-request budget
A single invocation may make at most 50 outbound fetch() calls. The ceiling is per invocation rather than per second, so it constrains the shape of the code and not the traffic the application receives. A design whose call count grows with its input, one request per item in a list, crosses the ceiling as soon as the list passes 50 items. This ceiling is a default too. To raise it for your plan, contact the technical support team.
The correction is to consolidate, replacing one call per member with one call that returns the set. That trade has its own boundary. The body a function can process is capped by plan: 100 MB on Hobby, 200 MB on Pro, and 500 MB on Enterprise. A response too large to process replaces one failure with another.
A body that large does not have to be held whole. WritableStream produces output incrementally instead of buffering an entire response in memory, and its backpressure keeps the function from overwhelming the destination while the data streams through. For the interface, refer to WritableStream.
A value that does not change between invocations can be kept rather than fetched again. KV Store persists it for a later invocation without an external API call.
Storage reached from the runtime
Azion Runtime reaches three stores from inside a function. Azion.KV reads and writes key-value pairs. Database.open() from azion:sql opens a connection to an SQL Database, and the Storage class from azion:storage reads and writes objects in a bucket. The KV Store API exists so that a function can persist and retrieve data without an external API call.
That property is what connects this decision to the one above. Data a function needs on every request can come from a store the runtime reaches directly, instead of a round trip to an origin.
Each interface carries a constraint worth knowing before code depends on it. Database.open() connects to the read replica of the database. An Object Storage bucket is created during the deploy of a static application, with Azion CLI or through Azion API. The function reads and writes objects in a bucket that already exists.
Static assets and dynamic routes can be served by the same function. The mountSPA and mountMPA helpers of the azion/utils package determine whether an incoming request is for a static asset or an application route, then fetch the matching resource. For both helpers, refer to Azion Utils package.
The split between Args and environment variables
A function’s code cannot be modified while it is being instantiated. What the instance sets instead is Args, a JSON object passed into the execution context of that instance. One function can therefore back several instances that behave differently, with no change to the code and no second copy of it.
Args and environment variables answer different questions. Args state how one instance behaves, and they belong to the instance that carries them. Environment variables hold the values that must not be hardcoded in the codebase, such as API keys, database credentials, and access tokens. A function reads one with Azion.env.get('API_SERVICE_TOKEN'). Each variable carries a secret field, a boolean that states whether the value is confidential. The --secret flag of Azion CLI sets that field, and the flag defaults to true.
The Args object caps at 100 KB, and an instance whose Args exceed it fails at instantiation. That ceiling is why Args carries configuration and not the data the function operates on. A payload that grows with the workload eventually stops the instance from being created at all. This ceiling is a default too. To raise it for your plan, contact the technical support team.
Log output and its destination
A function writes log messages with console.log, the same way a browser script does. The message becomes observable outside the platform when a Data Stream is configured with Functions as the source and the Functions Event Collector template. Each record carries the function identifier, the request identifier, the message, and a log level, one of ERROR, WARN, INFO, DEBUG, and TRACE.
Real-Time Events shows the same output in the Functions Console tab, and the fields of the record are what its filters work on. A past invocation can only answer what its logs already carry. That is why the levels and the identifiers are a design decision rather than an afterthought.
An exception raised while the function executes reaches the same logs, marked RUNTIME rather than CONSOLE. For what those entries carry and how to read them back, refer to ERROR entries in the function logs.
The instrumentation has a price of its own. Data Stream is billed on requests and data transfer, so the level a function logs at is a cost decision as much as a diagnostic one.