C++ on the Compute platform

The guidance on this page will work with the latest version (0.6.0) of the C++ SDK

The Compute platform supports application code written in C++, a language for high-performance applications.

Quick access

C++ language support

The C++ SDK for Compute uses the WASI C++ toolchain to build C++ applications to Wasm binaries.

In order to develop for Fastly Compute in C++, obtain the following tools for your platform:

This document assumes that cmake, make, and fastly are available on the system path, and that the WASI SDK is available at /opt/wasi-sdk.

HINT: For best results, expand the SDK to /opt/ and create a symbolic link (e.g., /opt/wasi-sdk) to maintain a consistent path across version updates.

Example (macOS Apple Silicon):

sudo tar -xzf ./wasi-sdk-32.0-arm64-macos.tar.gz -C /opt
sudo ln -sfn /opt/wasi-sdk-32.0-arm64-macos /opt/wasi-sdk
# On macOS, remove the provenance attribute to allow the compiler to run:
xattr -rd com.apple.quarantine /opt/wasi-sdk

For running unit tests, also obtain Viceroy (0.15.0 or newer) and place it on the system path.

Verify your environment

Before proceeding, ensure your tools are responsive:

cmake --version # Should be 3.25 or newer
head -1 /opt/wasi-sdk/VERSION # Should be 25 or newer
fastly version # Should be 14.0 or newer
# For unit testing
viceroy --version # Should be 0.15.0 or newer

Project layout

If you don't yet have a working toolchain and Compute service set up, start by getting set up.

A new project initialized from the default starter kit will contain a simple file tree:

├── .fastlyignore
├── .gitignore
├── CMakeLists.txt
├── CMakePresets.json
├── README.md
├── fastly.toml
└── src
└── main.cpp

Building and running the application

To turn your source code into a running service, you first configure the build system and then start the local testing server.

Configuration

The recommended way to manage the build process is to use CMake presets. The starter kit includes a CMakePresets.json file that defines the wasi preset, which has been set up to use the WASI-SDK toolchain.

📄 CMakePresets.json - "wasi" preset
json
{
"version": 3,
"configurePresets": [
{
"name": "wasi",
"generator": "Unix Makefiles",
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_TOOLCHAIN_FILE": "/opt/wasi-sdk/share/cmake/wasi-sdk-p1.cmake"
}
}
]
}

Configure the project by running cmake against this preset.

cmake --preset wasi

The build-and-serve Loop

The easiest way to build and test your application is using the Fastly CLI.

fastly compute serve

When you run the above command, the CLI looks at the [scripts.build] value in your fastly.toml file, and runs this build script automatically before starting the local server:

📄 fastly.toml - "scripts.build" script
TOML
[scripts]
build = "cmake --build build && cp build/main.wasm bin/main.wasm"

Once the server is running (usually on http://127.0.0.1:7676), you can test it by accessing it via a web browser, or by using curl in a separate terminal:

curl -i http://127.0.0.1:7676/

If you are running the default starter kit, this request will return a HTML response.

Key files

  • src/main.cpp: The entry point of your application, which contains the logic you'll run on incoming requests.
  • CMakeLists.txt: Describes your dependencies and build steps.
    • If you add more source files (like src/utils.cpp), you must add them to the add_executable() command in this file so the compiler knows to include them.
  • CMakePresets.json: Provides configuration presets (e.g., wasi for production, wasi-testing for testing).
  • fastly.toml: Contains metadata required by Fastly to deploy your package. Learn more about fastly.toml.

The Fastly dependency

The Fastly C++ SDK is added to the application using the FetchContent module in CMakeLists.txt. This automatically downloads the SDK at configuration time:

📄 CMakeLists.txt - Adding the Fastly SDK
cmake
include(FetchContent)
FetchContent_Declare(
fastly_sdk
URL https://github.com/fastly/compute-sdk-cpp/releases/download/v0.6.0/fastly-cpp-v0.6.0.tar.gz
DOWNLOAD_EXTRACT_TIMESTAMP NEW
SYSTEM
)
FetchContent_MakeAvailable(fastly_sdk)
find_package(fastly REQUIRED PATHS ${fastly_sdk_SOURCE_DIR} NO_DEFAULT_PATH)

The SYSTEM keyword enables the compiler to treat the SDK as a third-party library. Once configured, you can include the SDK in your source:

#include <fastly/sdk.h>

Main interface

Start a Compute program by defining a main() function:

📄 src/main.cpp - Minimal application
C++
#include <fastly/sdk.h>
int main() {
auto req = fastly::Request::from_client();
// Simple synthetic response
auto res = fastly::Response::from_status(200)
.with_body("Hello from the edge!");
res.send_to_client();
return 0;
}

The Fastly Compute C++ SDK provides the core object types such as Request, Response, and Body referenced by your application.

The program will be invoked for each request that Fastly receives for a domain attached to your service, and it is expected to send a response that can be served to the client.

The Request and Response objects

The Fastly C++ SDK uses the fastly::Request and fastly::Response classes to model HTTP traffic. These classes provide a structured way to capture, modify, and route data between your users and your backends.

Working with requests

An application typically interacts with requests in one of two ways:

  • Downstream (from the client): Use fastly::Request::from_client() to access the request sent by the user to the Fastly edge.
  • Synthetic: Use fastly::Request constructors to manually build a new request from scratch.

Once an object is instantiated, you can inspect or manipulate it using built-in methods such as:

  • Examine: Retrieve metadata using get_headers(), get_method(), or get_url().
  • Modify: Change behavior using set_ttl(), set_pass(), or by adding headers directly to the request with req.get_headers().insert(...).

Communicating with backend servers and the Fastly cache

A fastly::Request can be forwarded to any upstream backend defined on your service.

To retrieve content from an origin, you "send" a request to a defined backend.

📄 src/main.cpp - Sending a request to a named backend
C++
constexpr auto backend_name = "my_backend_name";
auto res = req.send(backend_name);

HINT: It's a good idea to define backend names as constants.

Requests forwarded to a backend will typically transit the Fastly readthrough cache interface, and the response may come from cache. See Readthrough (HTTP) cache for more precise or explicit control over the Fastly readthrough cache.

Additionally, the SDK supports dynamic backends created at runtime using fastly::backend::BackendBuilder.

Handling failures

Because network operations can fail, the .send() method returns a fastly::expected container.

📄 src/main.cpp - Handling failures
C++
auto result = req.send(backend_name);
if (!result) {
// Error path: Generate a local synthetic response
fastly::Response{}.with_status(502).send_to_client();
return 1;
}
// Success path: Send the backend response as-is
result->send_to_client(); // This is equivalent to result.value().send_to_client();

IMPORTANT: This is a common pattern in this SDK. Rather than utilizing traditional C++ exceptions, return values from such operations favor a safer and more explicit check, forcing a clean branching path in your code. A fastly::expected<T> will either contain a valid T or a FastlyError object.

To keep examples concise, this documentation assumes that calls succeeded. In a production environment, you should always check the fastly::expected result to handle potential system failures gracefully.

Handling and returning responses

A response is either returned from a backend fetch or created locally (a "synthetic response") to handle redirects or error pages.

  • Upstream responses: Take the successful return value from fastly::Request::send().
  • Creation: Instantiate fastly::Response() directly to create custom content.
  • Inspection: Use get_status() or get_headers() to verify the backend's data.
  • Downstream (send to client): Use .send_to_client() to stream the response back to the user. This is typically the final action your application takes.

NOTE: Calling .send_to_client() begins the streaming process. Once called, you can no longer modify the response headers or status code.

Parsing and transforming responses

The Fastly Compute platform is built for high-performance streaming. Request and Response in Compute services are streams, allowing large payloads to move through your service without running out of memory.

While you can read the entire body into memory using take_body_string(), this should be reserved for small payloads (like JSON metadata).

The following example reads a backend response into memory, replaces every occurrence of "cat" with "dog" in the body, and then creates a new body with the transformed string:

📄 src/main.cpp - Reading a response into memory
C++
auto api_resp = api_req.send("example_backend");
if (!api_resp) {
fastly::Response{}.with_status(500).send_to_client();
return 1;
}
auto api_resp_body = api_resp->take_body();
// Take care! take_body_string() will consume the entire body into
// memory, and regex_replace() will further double the memory requirement
auto api_resp_body_string = api_resp_body.take_body_string();
auto new_body = std::regex_replace(api_resp_body_string, std::regex{"cat"}, "dog");
api_resp->set_body(new_body);

For large streams, it is best to avoid loading the entire body into a single string or vector. Instead, use the body as a stream by calling read() in a loop to handle the contents piecewise. This allows your application to process massive payloads while maintaining a constant, minimal memory footprint.

In this example, we process a backend response 1 KiB at a time to calculate a basic checksum:

📄 src/main.cpp - Reading a response in a loop
C++
auto body = resp_result->take_body();
uint64_t total_checksum = 0;
std::vector<uint8_t> buffer(1024); // Fixed 1 KiB buffer
while (true) {
auto read_result = body.read(buffer.data(), buffer.size());
// Check for error or end of stream
if (!read_result || read_result.value() == 0) {
break;
}
// Perform calculation on the current chunk
size_t bytes_read = read_result.value();
total_checksum = std::accumulate(buffer.begin(), buffer.begin() + bytes_read, total_checksum);
// The buffer is reused in the next iteration,
// ensuring memory usage never exceeds ~1 KiB.
}
std::cout << "Processing complete. Checksum: " << total_checksum << std::endl;

C++ does not have built-in support for handling data such as HTML, JSON, XML, etc. Parsing these types of responses in C++ often benefits from well-tested third-party libraries. The following section describes an example of parsing JSON using such a library.

Parsing JSON responses

For structured data, the nlohmann/json library is a popular choice that works well on the Compute platform.

To use it, add the following to your CMakeLists.txt using the same FetchContent pattern used for the SDK:

📄 CMakeLists.txt - Using FetchContent to add a library
cmake
FetchContent_Declare(
json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.3
SYSTEM
)
FetchContent_MakeAvailable(json)
# Link it to your executable
target_link_libraries(main PRIVATE nlohmann_json::nlohmann_json)

HINT: Because modern C++ JSON libraries often rely on exceptions for error handling, ensure you have JSON_NOEXCEPTION defined or handle the -fno-exceptions flag as discussed in the "no-exceptions" rule section.

The following example attempts to parse a body as JSON, but limits the memory impact by only reading the first 4KiB of the stream.

📄 src/main.cpp - Parsing the first 4KiB of a body as JSON
C++
auto body = resp_result->take_body();
// Limit read to 4KiB to protect memory
std::vector<uint8_t> buffer(4096);
auto read_result = body.read(buffer.data(), buffer.size());
if (read_result && read_result.value() > 0) {
// Trim buffer to actual bytes read
buffer.resize(*read_result);
// Parse JSON safely. Note: with -fno-exceptions,
// nlohmann::json will call std::abort() on parse errors.
auto data = json::parse(buffer, nullptr, false);
if (!data.is_discarded()) {
if (data.contains("status") && data["status"] == "success") {
// ... logic based on JSON content ...
}
}
}

Compression

Fastly can compress and decompress content automatically, and it is often easier to use these features than to try to perform compression or decompression within your C++ code. Learn more about compression with Fastly.

Using edge data

Fastly provides high-performance data stores for configuration and state. All edge data resources are account-level, service-linked resources, allowing a single store to be accessed from multiple Fastly services.

Accessing these stores in C++ requires handling fastly::expected (for system errors) and std::optional (for missing data). Therefore, you should use the .has_value() function and/or the ! operator to check for and handle failures gracefully.

Config store

Best for application configuration. Note that for Config Stores, the result is nested: an expected containing an optional.

📄 src/main.cpp - Config Store
C++
auto store_res = fastly::config_store::ConfigStore::open("app_config"); // expected<ConfigStore>
if (!store_res) {
std::cout << "Failed to open Config Store" << std::endl;
fastly::Response::from_status(500).send_to_client();
return 1;
}
auto val_res = store_res->get("api_url"); // expected<optional<string>>
if (val_res && val_res->has_value()) {
// Access the inner string via the value() of the optional
std::cout << "API URL: " << val_res->value() << std::endl;
}

Secret Store

Used for sensitive data. This follows a similar pattern to the Config Store but returns a Secret object that must be explicitly decrypted.

📄 src/main.cpp - Secret Store
C++
auto store_res = fastly::secret_store::SecretStore::open("my_secrets"); // expected<SecretStore>
if (!store_res) {
std::cout << "Failed to open Secret Store" << std::endl;
fastly::Response::from_status(500).send_to_client();
return 1;
}
auto secret_res = store_res->get("api_key"); // expected<optional<Secret>>
if (secret_res && secret_res->has_value() ) {
auto key = secret_res->value().plaintext(); // std::string
// ... use key ...
}

KV Store

The KV Store is unique because it treats values as streams (just like HTTP bodies) and tracks versioning metadata.

When you call lookup(), you get a LookupResult which provides:

  • take_body(): Access the value as a Body.
  • metadata(): Access any custom metadata associated with the key.
  • current_generation(): A unique identifier for the specific version of the data (useful for concurrency control).

To write or update a value, call insert().

📄 src/main.cpp - KV Store
C++
auto store_res = fastly::kv_store::KVStore::open("sessions"); // expected<optional<KVStore>>
if (!store_res || !store_res->has_value()) {
std::cout << "Failed to open KV Store" << std::endl;
fastly::Response::from_status(500).send_to_client();
return 1;
}
auto kv_store = store_res->value();
// Read from KV Store
auto entry_res = kv_store.lookup("user_123"); // expected<LookupResponse>
if (entry_res && entry_res.has_value()) {
auto body_str = entry_res->take_body().take_body_string();
auto meta = entry_res->metadata();
auto version = entry_res->current_generation();
std::cout << "Data: " << body_str << " (Gen: " << version << ")" << std::endl;
}
// Write to KV Stores at runtime
auto kv_store_write_result = kv_store.insert("user_123", "active");
if (kv_store_write_result) {
std::cout << "Wrote to KV Store!" << std::endl;
}

For more advanced usages of the KV Store, such as for enumerating keys or using the builder pattern to query or update entries, see the C++ SDK documentation page on the KV Store.

Logging

Observability is your primary window into how your logic behaves at the edge. The C++ SDK provides flexible ways to output data, ranging from standard C++ streams to specialized, high-performance log endpoints.

Standard output logging (std::cout)

Any data sent to std::cout or std::cerr is captured by the Fastly runtime.

  • Locally: Logs appear in your local testing server terminal.
  • Production: Logs can be viewed in real-time using the fastly log-tail command.
std::cout << "Processing request for: " << req.get_url() << std::endl;

For additional information on logging for testing and debugging, refer to the Testing and debugging section below.

Real-time log streaming

For production environments, you typically want to send data to a named endpoint (e.g., S3, Datadog, or a custom syslog server). The SDK offers two ways to handle this:

  • Global logging (init_simple)

    This is the easiest way to add logging across your entire application. You initialize a global logger with a specific endpoint and a severity filter.

    📄 src/main.cpp - Global logging with init_simple()
    C++
    // Route all standard log calls to "my_endpoint_name"
    // and only send warnings or higher.
    fastly::log::init_simple("my_endpoint_name", fastly::log::LogLevelFilter::Warn);
    fastly::log::warn("This will be sent to the endpoint.");
    fastly::log::info("This will be ignored (below the 'Warn' threshold).");
  • Targeted logging (Endpoint::from_name())

    Use this when you need to send different types of data to different places (e.g., sending "Audit Logs" to S3 and "Performance Metrics" to Datadog).

    📄 src/main.cpp - Targeted logging with fastly::Endpoint::from_name()
    C++
    auto audit_log = fastly::log::Endpoint::from_name("audit_logs");
    audit_log << "Sensitive security event detected." << std::endl;

Structured JSON logging

To make your logs searchable in production, send them as JSON. This allows your log provider to index specific fields like status codes, durations, or custom IDs.

📄 src/main.cpp - Structured JSON logging
C++
#include <nlohmann/json.hpp>
nlohmann::json j = {
{"event", "checksum_calculated"},
{"value", checksum},
{"timestamp", std::time(nullptr)}
};
auto endpoint = fastly::log::Endpoint::from_name("analytics");
endpoint << j.dump() << std::endl;

Comparison of Logging Methods

MethodBest Use CaseLogic
std::coutDebugging / PrototypingStandard stream; visible in log-tail.
fastly::log::init_simple()General App LoggingGlobal filter; maps to fastly::log::info/warn/error.
fastly::log::Endpoint::from_name()Multi-destinationDirect control over specific named endpoints.

Using dependencies

The Compute build process compiles your C++ code to WebAssembly using the WASI-SDK. Because of this, it supports C++ libraries that are either freestanding or specifically compatible with the WASI sysroot.

The process of adding a dependency library to a C++ application varies more than other platforms. Refer to the instructions provided with the specific library.

Integrating libraries with FetchContent

For external dependencies that support it, an efficient method is CMake's FetchContent module. This automates the download and integration of source code during the configuration step.

📄 CMakeLists.txt - Adding nlohmann/json via FetchContent
cmake
include(FetchContent)
FetchContent_Declare(
nlohmann_json
GIT_REPOSITORY https://github.com/github/nlohmann/json.git
GIT_TAG v3.11.3 # Can be a tag, branch, or commit hash
SYSTEM # Suppresses warnings from dependency headers
)
FetchContent_MakeAvailable(nlohmann_json)
# Link the dependency to your application
target_link_libraries(your_app PRIVATE nlohmann_json::nlohmann_json)

Compatibility requirements

While many modern, header-only, or freestanding C++ libraries work seamlessly, the WASI system interface has strict limitations.

Feature CategoryLikely to Work?Notes
Header-only✅ YesLibraries like nlohmann/json or glm are highly compatible.
Algorithms/math✅ YesPure computational logic (e.g., Eigen, FastFloat) works well.
Multithreading❌ NoLibraries requiring <pthreads.h> or std::thread will fail to link.
Local networking❌ NoAccess to <sys/socket.h> is not supported in the WASI sandbox.
OS frameworks❌ NoAnything requiring a GUI (Win32, Cocoa) or local filesystems will fail.

Our Fiddle tool allows the use of an approved subset of modules tested for compatibility with the Compute platform. As a lightweight experimentation environment, Fiddle supports core functionality from these modules, though not all features of every module are supported.

  • Inja (3.5.0) (In Fiddle, use #include <inja.hpp>.)
  • toml++ (3.4.0) (In Fiddle, use #include <toml.hpp>.)
  • Magic Enum C++ (0.9.8) (In Fiddle, use #include <magic_enum/magic_enum.hpp>.)
  • nlohmann/json (3.12.0) (In Fiddle, use #include <nlohmann/json.hpp>.)
  • ada-url (3.4.4) (In Fiddle, use #include <ada.h>.)
  • stduuid (1.2.3) (In Fiddle, use #include <uuid.h>.)

The "no-exceptions" rule

Fastly Compute applications must be compiled with -fno-exceptions. This is a requirement for the WebAssembly environment to ensure predictable performance and smaller binary sizes.

When integrating third-party libraries, keep the following in mind:

  • Library configuration: Many libraries (like nlohmann/json) have specific macros to disable their internal exception handling (e.g., #define JSON_NOEXCEPTION).
  • Stubbing missing symbols: If a library compiles but fails at the linking stage with errors like undefined reference to __cxa_throw, it means the library is still trying to use C++ exceptions. Sometimes, stubbing them can get them to work.

    HINT: For example, in order to run Catch2, stub the following functions:

    Stubbing missing exception symbols
    C++
    #ifdef __wasm__
    extern "C" {
    void *__cxa_allocate_exception(size_t) { return nullptr; }
    void __cxa_throw(void *, void *, void *) { __builtin_trap(); }
    void __cxa_free_exception(void *) {}
    }
    #endif
  • Runtime behavior: When exceptions are disabled, most libraries will call std::abort() or __builtin_trap() if they encounter a fatal error. Ensure your input validation is robust so these error paths are never triggered.

Verifying whether a library is compatible

To verify if a library is compatible with Fastly Compute, attempt to configure your project with the WASI toolchain: cmake --preset wasi.

  1. Check for missing headers: Errors pointing to <sys/socket.h>, <pthreads.h>, or <dlfcn.h> indicate the library relies on unsupported OS features.
  2. Watch for linker errors: If the build fails at the linking stage with symbols like pthread_create or fork, the library is attempting to call unimplemented POSIX interfaces.
  3. Audit preprocessor macros: Some libraries use #ifdef _WIN32 or #ifndef __wasm__ to provide "no-op" stubs. Be aware that these stubs may return errors or trigger a std::terminate at runtime.

HINT: When using FetchContent, CMake automatically propagates your WASI toolchain settings to the dependency. This ensures the library is cross-compiled for WebAssembly specifically for your target environment.

To achieve the best possible performance and the smallest binary size, the C++ SDK supports Interprocedural Optimization (IPO), also known as Link-Time Optimization (LTO).

Standard compilation optimizes each .cpp file (translation unit) in isolation. LTO allows the linker to look across all files in your project to eliminate dead code and inline functions across the entire binary. This is particularly effective for WebAssembly, where binary size directly impacts startup time.

You can enable LTO in your CMakeLists.txt with the following block:

HINT: This is included by default in the starter kit.

📄 CMakeLists.txt - Enable LTO to help reduce Wasm binary size and improve performance
cmake
option(ENABLE_LTO "Enable cross language linking time optimization" ON)
if(ENABLE_LTO)
include(CheckIPOSupported)
check_ipo_supported(RESULT supported OUTPUT error)
if(supported)
message(STATUS "IPO / LTO enabled")
# Tells the compiler to optimize across different translation units
set_target_properties(main PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE)
# Force the use of the LLVM Linker (lld), which is required for Wasm LTO
target_link_options(main PRIVATE -fuse-ld=lld)
else()
message(STATUS "IPO / LTO not supported: <${error}>")
endif()
endif()

By using LTO, you ensure that the final Wasm module is as lean as possible, reducing the "Cold Start" time of your service when it is first invoked at the edge.

Testing and debugging

Logging to standard output (via std::cout, printf(), etc.) is the primary way to observe how your program behaves at the edge.

Log output from live services can be monitored via live log tailing. The local test server and Fastly Fiddle display all log output automatically. See Testing & debugging for more information about choosing an environment in which to test your program.

  • Local development server You can run your application on your local machine in a local testing server. It creates a local version of the Fastly environment, allowing you to run your Wasm binary and see exactly how it behaves before deploying.

    All std::cout and std::cerr output is piped directly to your terminal.

    fastly compute serve
  • Live log tailing Once your service is live, you can use log tailing to monitor std::cout and std::cerr output from your globally deployed application instances in real-time from your console. This is essential for debugging issues against real customer traffic, or for features that don't have support in the local testing server.

    # View production logs as they happen
    fastly log-tail
  • Fiddle If you are testing your application using the Fastly Fiddle tool, it displays std::cout and std::cerr output in the Fiddle interface.

Debugging request and response bodies by "peeking"

Most common logging requirements involve inspecting HTTP requests and responses. However, since HTTP bodies are forward-only streams, they can only be consumed once. This means that simply reading a body to produce a log message will disrupt the rest of your program.

To solve this, you can use a body restoration pattern. To "peek" at a body, we read a small amount of data into a local buffer, then create a new body containing those bytes and append the original stream to it. Using a C++ Template allows this logic to work seamlessly for both Request and Response objects. With the template helper, logging request and response data becomes a clean one-liner that doesn't interfere with your send() or send_to_client() calls.

📄 src/main.cpp - "peeking" into request and response bodies
C++
// We use a template 'T' so this works for both Request and Response
template <typename T>
std::string peek_and_restore(T& message, size_t len) {
// 1. Take the body from the message
auto body = message.take_body();
// 2. Read the prefix
std::vector<uint8_t> buffer(len);
auto read_res = body.read(buffer.data(), buffer.size());
if (!read_res || *read_res == 0) {
message.set_body(std::move(body)); // Put it back if empty
return "";
}
size_t n = *read_res;
std::string prefix(reinterpret_cast<char*>(buffer.data()), n);
// 3. Restore the body by prepending the bytes we read
auto restored_body = fastly::Body();
if (restored_body.write(buffer.data(), n)) {
restored_body.append(std::move(body));
}
// 4. Put the restored body back into the message
message.set_body(std::move(restored_body));
return prefix;
}
std::cout << "Request:" << std::endl;
std::cout << "User-Agent: " << header_val(req.get_header("User-Agent").value()) << std::endl;
std::cout << "POST Body: " << peek_and_restore(req, 1024) << std::endl;
std::cout << "Response:" << std::endl;
std::cout << "Content-Type: " << header_val(beresp.get_header("Content-Type").value()) << std::endl;
std::cout << "Response Prefix: " << peek_and_restore(beresp, 20) << std::endl;

HINT: This pattern is highly efficient. Only the "peeked" bytes are buffered in memory; the rest of the body continues to stream through the Fastly host handles.

Handling WebAssembly traps

When a C++ program performs an illegal operation (like accessing a failed fastly::expected via .value()), it "Traps."

  • In the local testing server: You will see a detailed error message and potentially a stack trace in your terminal.
  • In production: The visitor will receive a 500 Internal Server Error.

IMPORTANT: In a production-ready service, always check expected.has_value() before calling .value(). A trap in production is difficult to debug because it provides no custom log data before termination.

Unit testing

Because the Fastly C++ SDK relies on WebAssembly hostcalls (functions provided by the Fastly runtime), you cannot use standard native tools to test SDK-dependent code directly. If you try to run a test that would access a fastly object, the program will crash because the Host environment isn't there to answer.

One recommended strategy is to employ the Interface/Logic separation pattern. By writing your application logic to accept generic interfaces rather than concrete SDK objects, you can run tests against "Mock" classes without relying on code that invokes hostcalls.

Catch2 is a simple testing framework that is compatible with the Compute C++ SDK. This section describes how to set up a C++ application that uses Catch2 with this strategy.

Project structure

Use a standard C++ layout to keep your headers, implementation, and tests distinct:

my-service/
├── include/
│ ├── interfaces.h # Abstract base classes
│ └── utils.h # Headers for utils.cpp
├── src/
│ ├── main.cpp # The Fastly entry point
│ └── utils.cpp # Business logic implementation
├── tests/
│ ├── main.cpp # Test entry point
│ └── test_utils.cpp # Catch2 test cases
├── CMakeLists.txt
├── CMakePresets.json
└── fastly.toml

In our somewhat-contrived example below, we will be writing and testing is_mobile(), a function that tests a request's User-Agent header to detect whether it has been sent from a mobile device.

Define the interface

Instead of passing a fastly::Request directly to your functions, we will define an interface that lets the caller get a header value. This allows you to swap a real Request for a "Mock" Request during testing.

📄 include/interfaces.h
C++
#pragma once
#include <string>
#include <optional>
// An interface for any object that can provide headers
class HeaderSource {
public:
virtual ~HeaderSource() = default;
virtual std::optional<std::string> get_header(const std::string& name) const = 0;
};
📄 src/utils.h
C++
#pragma once
#include "interfaces.h"
bool is_mobile(const HeaderSource& source);

Write the logic

This is the code we want to test. The business logic remains "pure". It doesn't know about the Fastly SDK; it only knows about the HeaderSource interface.

📄 src/utils.cpp
C++
#include "utils.h"
// Logic that determines if a request is from a mobile device
bool is_mobile(const HeaderSource& source) {
auto ua = source.get_header("User-Agent");
if (!ua) return false;
return ua->find("Mobile") != std::string::npos;
}

Write the Catch2 test cases

Using Catch2, we can structure our tests into TEST_CASE and SECTION blocks for high readability.

📄 tests/test_utils.cpp
C++
#include <catch2/catch_test_macros.hpp>
#include "interfaces.h"
#include "utils.h"
// A Mock implementation for testing
class MockSource : public HeaderSource {
std::string mock_ua;
public:
MockSource(std::string ua) : mock_ua(ua) {}
std::optional<std::string> get_header(const std::string& name) const override {
if (name == "User-Agent") return mock_ua;
return std::nullopt;
}
};
TEST_CASE("Device detection logic", "[utils]") {
SECTION("Detects mobile devices") {
MockSource mobile_req("Mozilla/5.0 (iPhone; CPU iPhone OS 14_0) Mobile");
CHECK(is_mobile(mobile_req) == true);
}
SECTION("Detects desktop devices") {
MockSource desktop_req("Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
CHECK(is_mobile(desktop_req) == false);
}
SECTION("Handles missing User-Agent") {
MockSource empty_req("");
CHECK(is_mobile(empty_req) == false);
}
}

The test entry point

Since we compile with -fno-exceptions, we need to define CATCH_CONFIG_RUNNER and provide a main() to run our Catch session.

📄 tests/main.cpp
C++
#define CATCH_CONFIG_RUNNER
#include <catch2/catch_all.hpp>
#ifdef __wasm__
extern "C" {
// Stubs needed to get Catch2 to link
void *__cxa_allocate_exception(size_t) { return nullptr; }
void __cxa_throw(void *, void *, void *) { __builtin_trap(); }
void __cxa_free_exception(void *) {}
}
#endif
int main(int argc, char* argv[]) {
// Add wasi-specific setup here if needed
return Catch::Session().run(argc, argv);
}

Build configuration

To run these Catch2 tests, we compile them into a WebAssembly binary and execute them inside Viceroy, the local testing server.

📄 CMakeLists.txt (snippet) - Add test configuration
cmake
# Catch2 testing utility
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.4.0
SYSTEM
)
add_compile_definitions(CATCH_CONFIG_NO_POSIX_SIGNALS)
add_compile_definitions(CATCH_CONFIG_DISABLE_EXCEPTIONS)
FetchContent_MakeAvailable(Catch2)
add_executable(unit_tests
tests/main.cpp
tests/test_utils.cpp
src/utils.cpp
)
target_include_directories(unit_tests PRIVATE include)
if(NOT BUILD_TESTING)
# NOTE - Move this step into here
target_link_libraries(main PRIVATE
fastly::fastly
)
# Don't build unit_tests app
set_target_properties(unit_tests PROPERTIES EXCLUDE_FROM_ALL TRUE)
else()
# Link the unit_tests app
target_link_libraries(unit_tests PRIVATE
fastly::sdk
Catch2::Catch2WithMain
)
# Don't build the main app
set_target_properties(main PROPERTIES EXCLUDE_FROM_ALL TRUE)
endif()
📄 CMakePresets.json (snippet) - Add preset
json
{
"name": "wasi-tests",
"generator": "Unix Makefiles",
"binaryDir": "${sourceDir}/build-tests",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"BUILD_TESTING": "ON",
"CMAKE_TOOLCHAIN_FILE": "/opt/wasi-sdk/share/cmake/wasi-sdk-p1.cmake"
}
}

Running the tests

Now you can build your test suite as a Wasm binary and execute it using Viceroy. Viceroy provides the necessary WebAssembly environment so the Wasm binary can execute safely.

# 1. Configure and Build
cmake --preset wasi-tests
cmake --build build-tests
# 2. Run the tests inside the Viceroy simulation
viceroy run build-tests/wasm_tests.wasm

The following example output is produced:

Randomness seeded to: 1154335898
===============================================================================
All tests passed (3 assertions in 1 test case)

This workflow gives you the best of both worlds: your code is architected to be modular and testable, but you are still verifying the final binary in the actual WebAssembly environment it will inhabit.

The application entry point

In your application's code, you:

  • Create a concrete implementation of the interface that talks to the Fastly SDK
  • Coordinate the execution by wrapping the incoming request and passing it to your business logic
📄 src/main.cpp
C++
// Concrete implementation of HeaderSource interacts with actual Fastly Request object.
class FastlyHeaderSource : public HeaderSource {
fastly::Request& _req;
public:
FastlyHeaderSource(fastly::Request& req) : _req(req) {}
std::optional<std::string> get_header(const std::string& name) const override {
auto res = _req.get_header(name); // returns expected<optional<HeaderValue>>
if (res && res->has_value() && res->value().string().has_value()) {
return std::string(res->value().string().value());
}
return std::nullopt;
}
};
// 1. Initialize the incoming request from the client
auto req = fastly::Request::from_client();
// 2. Wrap the request in our interface implementation
FastlyHeaderSource source(req);
// 3. Use our testable business logic!
bool mobile = is_mobile(source);
// 4. Build and send the response
auto resp = fastly::Response::from_status(200);
resp.with_body(mobile ? "Hello, mobile user!" : "Hello, desktop user!");
resp.send_to_client();

By structuring your code this way, your main.cpp becomes a thin "adapter" layer.

  • The SDK is isolated: Only this file and your FastlyHeaderSource know about the Fastly SDK types.
  • The logic is pure: Your utils.cpp stays clean, portable, and 100% testable on any platform.
  • The tests are fast: You can run thousands of unit tests against your is_mobile logic using the MockSource without ever needing to compile a Wasm binary or start Viceroy.

Key takeaways and next steps

The Fastly C++ SDK gives you the closest possible access to the metal of the edge. Now that you have the tools to build, test, and debug effectively, you're ready to push the boundaries of what's possible at the network's edge.

As you work with C++ on Fastly Compute, keep in mind the following:

  • Embrace move semantics: Understand that fastly::Body and fastly::Request are move-only resources that represent live system handles.
  • Trust but verify: Handle the expected<T> return values to navigate the layers of potential failure at the edge.
  • Architect for testability: Keep your core logic "pure" so your code is testable without relying on concrete Fastly object.