2026年11月14日
2026年11月14日
Offload Heavy WordPress Tasks to AWS Lambda via Function URL
This article is a localized edition of the Japanese original. Read the Japanese original
Running time-consuming work such as image processing, bulk fetching from external services or PDF generation inside a WordPress (PHP) request slows down page views and saves. This article shows, with steps and code, how to hand that work to AWS Lambda and store only the result in WordPress.
The AWS specifications in this article were checked against the official AWS documentation as of November 2026. The WordPress code was tested on WordPress 7.1.3 (PHP 8.3), and the Lambda code was tested by passing it requests in the same format a function URL sends.
How it works
- When a post is published or updated in WordPress, an event "ask Lambda to process this" is scheduled in WP-Cron (the save itself does not wait).
- WP-Cron POSTs to the Lambda function URL. To prevent tampering and spoofing, the request carries an HMAC-SHA256 signature made with a shared secret key.
- Lambda verifies the signature, does the work and returns the result as JSON.
- WordPress saves the result in a custom field.
In this article the "work" is just counting the characters of the post body. In practice, you replace that part with your own heavy processing.
There are two ways to call Lambda over HTTP: function URLs and Amazon API Gateway. For a simple use where only WordPress calls the function, a function URL is enough. If you need a custom domain, usage limits or central management of several APIs, consider API Gateway.
Preparation
- Install AWS CLI v2 and set your region (for example
ap-northeast-1) and credentials withaws configure. - Create a shared secret key. The following command produces a random 64-character string.
openssl rand -hex 32
Step 1: Create an IAM role for Lambda
Create the execution role the Lambda function uses and attach permission to write logs to CloudWatch Logs (the AWS managed policy AWSLambdaBasicExecutionRole).
trust-policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "lambda.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
aws iam create-role \
--role-name wp-heavy-task-role \
--assume-role-policy-document file://trust-policy.json
aws iam attach-role-policy \
--role-name wp-heavy-task-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Note the Arn shown in the create-role output (arn:aws:iam::<account-id>:role/wp-heavy-task-role).
Step 2: Create the Lambda function
Create lambda_function.py. It takes the headers and body from the request passed by the function URL (payload format version 2.0), checks the signature and timestamp, and only then does the work.
import base64
import hashlib
import hmac
import json
import os
import time
SECRET = os.environ["SHARED_SECRET"].encode("utf-8")
def response(status, data):
return {
"statusCode": status,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(data, ensure_ascii=False),
}
def lambda_handler(event, context):
if event.get("requestContext", {}).get("http", {}).get("method") != "POST":
return response(405, {"error": "POST only"})
headers = {k.lower(): v for k, v in (event.get("headers") or {}).items()}
body = event.get("body") or ""
if event.get("isBase64Encoded"):
body = base64.b64decode(body).decode("utf-8")
# Verify the signature (reject requests more than 5 minutes off and requests whose signature does not match)
ts = headers.get("x-mysite-timestamp", "")
sig = headers.get("x-mysite-signature", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
return response(401, {"error": "bad timestamp"})
expected = hmac.new(SECRET, (ts + "." + body).encode("utf-8"), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
return response(401, {"error": "bad signature"})
data = json.loads(body)
text = data.get("content", "")
# Put the time-consuming work here (this example only counts characters)
result = {"post_id": data.get("post_id"), "chars": len(text)}
return response(200, result)
Zip it and create the function. Replace <account-id> and the secret key with your own. The runtime is python3.14, which is supported as of November 2026 (runtimes such as Python 3.8 are already out of support).
zip function.zip lambda_function.py
aws lambda create-function \
--function-name wp-heavy-task \
--runtime python3.14 \
--handler lambda_function.lambda_handler \
--role arn:aws:iam::<account-id>:role/wp-heavy-task-role \
--zip-file fileb://function.zip \
--timeout 60 \
--environment "Variables={SHARED_SECRET=your-secret-key}"
Right after creating the role, you may get an error saying the role cannot be used yet. In that case, wait a few seconds and run the command again.
--timeout is the maximum run time of the function in seconds; the default is 3 seconds and the maximum is 900 seconds (15 minutes). Set it to match how long your work takes. Work longer than 15 minutes cannot be done in a single Lambda invocation, so split the work or consider spreading it over several runs with Amazon SQS or AWS Step Functions.
Step 3: Create the function URL and allow invocations
aws lambda create-function-url-config \
--function-name wp-heavy-task \
--auth-type NONE
The FunctionUrl in the output (in the form https://<url-id>.lambda-url.<region>.on.aws/) is the endpoint to call.
Even with auth type NONE, the function URL returns 403 unless the function's resource-based policy allows public access. According to the AWS documentation, function URLs created from October 2025 onward need both the lambda:InvokeFunctionUrl and lambda:InvokeFunction permissions. The console adds them automatically; with the CLI you add them with two commands.
aws lambda add-permission \
--function-name wp-heavy-task \
--statement-id UrlPolicyInvokeURL \
--action lambda:InvokeFunctionUrl \
--principal "*" \
--function-url-auth-type NONE
aws lambda add-permission \
--function-name wp-heavy-task \
--statement-id UrlPolicyInvokeFunction \
--action lambda:InvokeFunction \
--principal "*" \
--invoked-via-function-url
Quote --principal "*" (unquoted, the shell may expand it to file names). With --invoked-via-function-url, the permission does not apply to invocations through any route other than the function URL.
With auth type NONE, anyone who knows the URL can call the function. That is why the code in Step 2 verifies the signature and returns 401 without processing when it does not match. You can use IAM authentication (AWS_IAM) instead, but then WordPress has to create SigV4 signatures.
Step 4: Test the call with curl
Send a signed request and an unsigned one and compare the results.
FUNCTION_URL="https://<url-id>.lambda-url.ap-northeast-1.on.aws/"
SECRET="your-secret-key"
body='{"post_id":1,"content":"test"}'
ts=$(date +%s)
sig=$(printf '%s' "${ts}.${body}" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $NF}')
# Signed → {"post_id": 1, "chars": 4}
curl -s -X POST "$FUNCTION_URL" \
-H "Content-Type: application/json" \
-H "X-Mysite-Timestamp: ${ts}" \
-H "X-Mysite-Signature: ${sig}" \
--data "$body"
# Unsigned → {"error": "bad timestamp"} (HTTP 401)
curl -s -X POST "$FUNCTION_URL" -H "Content-Type: application/json" --data "$body"
Step 5: Call it from WordPress
First, put the function URL and the secret key in wp-config.php (above the require_once ABSPATH . 'wp-settings.php'; line). This keeps the secret out of your theme and plugin files.
<?php
define( 'MYSITE_LAMBDA_URL', 'https://<url-id>.lambda-url.ap-northeast-1.on.aws/' );
define( 'MYSITE_LAMBDA_SECRET', 'your-secret-key' );
Save the following code as wp-content/mu-plugins/mysite-lambda.php (create the mu-plugins folder if it does not exist).
<?php
/**
* Plugin Name: Lambda integration sample
* Description: When a post is published or updated, asks a Lambda function URL to process it and saves the result in a custom field.
*/
// 1. When a post is published or updated, queue the Lambda request in WP-Cron (saving does not wait for it)
add_action( 'save_post_post', function ( $post_id, $post ) {
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) || 'publish' !== $post->post_status ) {
return;
}
if ( ! wp_next_scheduled( 'mysite_lambda_task', array( $post_id ) ) ) {
wp_schedule_single_event( time(), 'mysite_lambda_task', array( $post_id ) );
}
}, 10, 2 );
// 2. Call Lambda inside WP-Cron and save the result
add_action( 'mysite_lambda_task', function ( $post_id ) {
if ( ! defined( 'MYSITE_LAMBDA_URL' ) || ! defined( 'MYSITE_LAMBDA_SECRET' ) ) {
return;
}
$post = get_post( $post_id );
if ( ! $post ) {
return;
}
$body = wp_json_encode(
array(
'post_id' => $post_id,
'content' => wp_strip_all_tags( $post->post_content ),
)
);
$timestamp = (string) time();
$signature = hash_hmac( 'sha256', $timestamp . '.' . $body, MYSITE_LAMBDA_SECRET );
$response = wp_remote_post(
MYSITE_LAMBDA_URL,
array(
'timeout' => 60,
'headers' => array(
'Content-Type' => 'application/json',
'X-Mysite-Timestamp' => $timestamp,
'X-Mysite-Signature' => $signature,
),
'body' => $body,
)
);
if ( is_wp_error( $response ) ) {
error_log( 'Lambda error: ' . $response->get_error_message() );
return;
}
$code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $code ) {
error_log( 'Lambda HTTP ' . $code . ': ' . wp_remote_retrieve_body( $response ) );
return;
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( isset( $data['chars'] ) ) {
update_post_meta( $post_id, '_mysite_char_count', (int) $data['chars'] );
}
} );
Key points:
save_post_postis a hook that runs only when a post of the "post" type is saved. Revisions, autosaves and statuses other than published are excluded.- The Lambda call is handed to WP-Cron with
wp_schedule_single_event(), so saving in the editor does not wait for Lambda. - Set the
timeoutofwp_remote_post()to at least the Lambda--timeout. If it is shorter, WordPress gives up while Lambda is still working. - The signature is computed over "timestamp +
.+ the exact body being sent". Lambda computes it over the exact body it receives, so differences in JSON formatting cannot cause a mismatch.
Check it with WP-CLI:
# Publishing a post registers the scheduled event
wp cron event list --fields=hook,args,next_run_relative
# Run due events now
wp cron event run --due-now
# Check the result (example: post ID 13)
wp post meta get 13 _mysite_char_count
In the test environment, after publishing a post whose body was こんにちは世界 ("Hello world" in Japanese, 7 characters) and running wp cron event run --due-now, 7 was saved in _mysite_char_count.
Step 6: Check the logs
Output from print() and errors in Lambda are recorded in the CloudWatch Logs log group /aws/lambda/<function-name>. With AWS CLI v2 you can follow them with this command:
aws logs tail /aws/lambda/wp-heavy-task --follow
Failures on the WordPress side (timeouts, 401 and so on) are written to the PHP error log by error_log().
Operational notes
- WP-Cron runs when the site gets visits. On low-traffic sites, runs are delayed, so consider running
wp cron event run --due-nowregularly from the server's cron. - There is no retry on failure. This sample only logs failures. If you want failed work to be retried, add something like re-scheduling with
wp_schedule_single_event()a little later after a failure. It is safer to make the work produce the same result even if the same post is processed twice. - Handling the secret key. Lambda environment variables can be read by anyone who can view the Lambda configuration. For stricter control, consider AWS Secrets Manager. When you change the secret, update both Lambda and
wp-config.php. - Pricing. Lambda charges by the number of requests and the run time (which depends on the memory you allocate). Check the AWS pricing page for current prices.
- This is different from running WordPress itself on Lambda. In this article, WordPress runs on a normal server and only the heavy work is handed to Lambda.
Related articles
- How to set up AWS Lambda + API Gateway (an HTTP API endpoint for WordPress forms)
- How to implement scheduled tasks with WP-Cron
- How to fix WP-Cron not running in WordPress
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)