Learn Functions Patterns and Best Practices

This section has general development patterns and best practices for Functions developers.

Use Patterns for Apex Developers

If you're an Apex developer, you may have complex business logic implemented in Apex that you’d like to move to a Salesforce Function. Moving your code to a Function lets you take advantage of the elastic Salesforce Functions compute environments, so you no longer have to worry about Apex governor limits like CPU time.

However, since Functions code runs “outside” of your org, certain things like transactions and data access work differently from Apex code running in your org. Consider the following development patterns when converting your Apex code into Functions.

Good error handling in Functions code is as important as in any other code you write. We recommend using language semantics (like try/catch/finally) that support error handling.

Error conditions to be more aware of:

  • Errors or exceptions from your own code
  • Errors or exceptions from library code you import
  • Errors or exceptions from Functions SDK methods
  • Errors or exceptions that come from HTTP callouts
  • Errors or exceptions from Functions SDK methods or Salesforce REST APIs that relate to standard Salesforce REST API limitations, such as, but not limited to, Record Locks, Concurrency Limits, General Record Validation errors, for more information see [link] to the Salesforce REST API

Here are some considerations for routing errors to your users:

  • Encoding in the response returned from the Function and allow your Apex logic to route the error accordingly
  • Use the Functions SDK to send a Platform Event (not possible if the root cause is a Salesforce API limit issue)
  • Return an error status code and short message in the Function response AND write to the Function Log. Ensure that your Salesforce Admin has set up Logdrains and is monitoring for these events.

Manage Transactions in Your Function

Functions run outside the Apex transaction from which they were invoked. Data changes made in a Function aren’t automatically tracked as part of a transaction, and therefore can’t be rolled back as part of an Apex transaction. Similarly, if your Function crashes before committing all data changes, partial changes aren't automatically rolled back, and uncommitted changes aren't automatically tracked. If you need transactions within your Function, you must manage transactions yourself.

To manage transactions in a Function, use the UnitOfWork SDK class. UnitOfWork can be used to add data operations to a transaction that can be committed as a single operation. Use the register methods, such as registerCreate() or registeredDelete(), to register data operations and then commitUnitOfWork() to commit the set of operations. Until you commit, nothing is changed in your org’s data.

As an example, suppose you had the following Apex code in a class method:

This method would be treated as one transaction, so if any exception or error occurred between inserts, all data changes made in this method would get automatically rolled back.

If you convert this Apex code into JavaScript using several context.org.data.insert() calls, you run the risk of not being able to roll back data changes if an error occurs. To properly convert the whole Apex transaction into JavaScript, you’d use UnitOfWork to encapsulate the transaction:

Take Advantage of Higher Compute Limits

Because Functions run outside your org in Salesforce Functions compute environments, Function code can be run without worrying about Apex compute limits. Your Apex code may be designed to accommodate things like Apex governor CPU limits, so you may be able to improve the efficiency and manageability of your code when you port to Salesforce Functions.

For example, you may have opted to use Apex Maps to pre-fetch and pre-process query results to reduce CPU use. In your Function, you may be able to avoid this pre-process step. Similarly, if you are hitting CPU limits while doing things like date/time comparisons across large data sets, you can do the same set of comparisons in a Function without hitting a CPU limit.

Follow best practices around making your queries and DML more efficient, as data operations from Functions are governed by API call limits, not CPU limits.

Avoid Excessive Data API Calls

Currently API requests from Functions are counted against the maximum API requests for a given org.

Orgs using Functions are granted 235,000 API requests per day for the sole use by functions (like a separate bucket of API requests). There are however other Platform API limits to consider - for example a Salesforce org enforces a limit of 25 active long running API transaction requests.

Unlike Apex, Functions use Salesforce Web Service API calls to access org data. There are various API call limits, such as the number of API calls made during a 24-hour period, and record size limits. Your Apex code likely wasn’t written with these limits in mind, so consider using the following approaches to avoid excessive API calls in your Function:

  • Use UnitOfWork. Along with transaction support, UnitOfWork uses the Composite Graph API to batch together several operations into a single API call.

  • Consider doing data changes in Apex rather than in the Function, possibly passing information about records that to be changed back from the Function to Apex in the Function response data. This technique lets you do the actual data operations in the Apex code that originally invoked your Function.

    The following code sample shows how Objects returned in a Callback payload are updated.

  • In some scenarios you can do some data operations in Apex before invoking the Function and pass the results to the Function via the Function invoke payload. Keep your payload size to 6 MB or less (12 MB for asynchronous invocations).

  • For large data operations that can be performed asynchronously, consider using the Bulk API 2.0 from your Function. The Function payload provides a Salesforce API token for the invoking org that you can use when making HTTP REST API calls to Salesforce APIs like Bulk API 2.0.

It also helps to monitor API usage of your Function. Salesforce provides various APIs and tools to check current API usage. For more information on API limits and how to monitor API usage, see Monitoring Your API Usage.

See Limits for more details on API limits and other comparable limits when working with Functions.

Use Functions Permission Set

Your Apex code typically runs as the current user, in Apex system mode, and can have broad object permissions, as described in Understanding Describe Information Permissions. Your Function code runs outside of your org and has different permissions. Deployed Functions use the "Functions" permission set to determine object access.

If you're converting your Apex code into Function code, review what object permissions your Function code needs and update the "Functions" permission set accordingly.

For more details on Functions permissions, see Function Permissions

Learn General Best Practices

Keep the following best practices in mind while developing Functions.

Compare Synchronous vs. Asynchronous Invocation Timeouts

A synchronously invoked function can run for 2 minutes before the invocation will timeout. This limit is enforced in Core and by Apex with the following error message:

System.CalloutException: Read timed out

The function, however, continues to run to completion. Also, since the Apex callout has terminated, the response from the invocation won't be received. Apex invoking a function that times out must recover from this kind of error. Consider also that Function code continues to run and can still communicate back with the org - inserting records (thus generating CDC events) or sending Platform Events itself. This behavior will change in the future when the timeout is enforced by providing a means to terminate long running invocations.

For asynchronous invocations, the function can run for up to 15 minutes. This is a published but currently unenforced limit. This timeout limit will be enforceable in future updates for Functions, meanwhile, as a best practice, avoid Functions running for longer than 15 minutes.

Compare Interactive vs. Batch Invocations

For interactive, real-time use cases when a user is waiting for a response, an invocation can be synchronous or asynchronous. Response time and function execution duration depends on what the function does.

For batch processing, a function can be invoked faster than your org can handle requests coming from your Function to query or update record data in your objects. Be sure to consider the rate at which functions are invoked to ensure their response or callback don't exceed daily API limits for Functions. Alternatively, you can also decide to implement database operations in the calling or responding Apex logic after the function invocation is complete.

For example, Batch Apex job execution is limited by the compute work it performs, and such jobs execute faster if they're mostly tasked with invoking asynchronously Functions. As such if you're invoking Functions from this context keep in mind the data access those Functions perform and their usage of the Functions Daily API limit.

Bulkify Functions and Payloads When Batch Processing

If you're using your Function to do batch processing of your org data, consider bulkifying the work within the Function itself. For example, instead of invoking a Function to process a single record (and then invoking that Function multiple times, possibly in a loop, from Apex), consider passing a set of record IDs that the Function can work on. You can send up to a 6-MB payload (12 MB for async invocation) per invocation. This way, you can use that payload limit to send a batch of IDs, or even a SOQL query string that the Function can use to get the set of records to operate on.

Understand Process Memory

For both synchronous and asynchronous invocation, there's a limit of 1 GB for process memory for each function container.

Salesforce Functions automatically increases or decreases the number of function containers available to handle invocations based on CPU load. Each function container can handle multiple concurrent invocations. As a result, the total available memory in a function container is shared across all concurrent invocations.

If the total memory footprint of the concurrently running invocations (and other overhead) exceeds the total memory available in a Function container, the platform shuts down the container and start a new one. Currently executing invocations in that container fail. An Out of Memory message is logged in the functions log stream.

Use Out of Memory Errors

Here are some best practices to follow to stay within memory limits while developing with Functions:

  • Keep your Functions simple and focused on a specific task that can complete quickly
  • Deploy your functions with just the libraries that you actually use to conserve memory
  • If you connect to external services from functions, profile response times for those services
  • Test your functions in development environment with loads that you expect in your production environment to make sure the response times are acceptable.
  • Find average execution time and memory use per invocation by testing your Funcitons

Don't Rely on Function State

Salesforce Functions are run in a stateless model. Nothing is automatically persisted from invocation to invocation, so your Function can't make any assumptions about system caches or filesystem content. Similarly, your Function can't rely on information generated in a previous run. If your Function must persist state or information between invocations, you must manage this information in a separate data store you control (this could be data in your org, such as a custom object or Apex platform cache). If your Function must have an initial state of variables at run time, set that state in your Function code that gets run during Function invocation, and not in a global variable or external package.

Don't make any assumptions on the run time of an invocation. Salesforce Functions launch and run your Function as efficiently as possible, but the run time for a Function isn't guaranteed to be the same for every invocation. It's even possible for Functions to finish “out of order” — for example, depending on the complexity and dependencies of a Function container, an initial run of a Function can take longer than subsequent runs, and therefore a second run of a Function could finish before the first run.

Understand Sharing State Across Invocations

It's possible to share state across invocations inadvertently through the use of global variables and temporary files in the filesystem. By default don't use global variables or create a file or folder with a static name. Generate random file and folder names so that multiple invocations of the same function don't overwrite each other’s data.

In certain cases, you want to create a file that can be shared across multiple invocations. The first invocation of the function could create a file and process some data that could be used by subsequent invocations, improving performance of your functions code. However, this is an advanced use case. Take care to design and develop code that runs as expected. State sharing is only applicable to current container, which isn't guaranteed to be the same container instance in subsequent responses.