Networking & Content Delivery

Leveraging Lambda@Edge for AdTech: Cookie Syncing at the Edge

In online advertising, cookies play a key role in identifying and profiling users. They allow advertisers to display targeted and personalized ads to users as they browse the internet, from a desktop browser or mobile browser.

Advertisers can achieve this personalization by assigning a cookie to users when their ads are displayed on websites or by placing tracking pixels on web pages. This allows advertisers to assign a cookie to online users with whom they have no direct contact or relationship.

But cookies have a one big limitation: they are domain specific, meaning that they can only be read by the domain that created them. This creates quite a headache for advertisers, advertising technology (or “AdTech”) vendors, and publishers who want to share user-level data across platforms to improve targeting and enhance media buys.

To get around the domain limitation and share information and data about online users across different platforms, advertisers, AdTech vendors, data providers, and publishers must implement something called cookie syncing.

Cookie syncing involves mapping cookies from one AdTech platform to another. While the concept itself is straightforward, because of high request rates, cookie syncing infrastructure design requires some careful consideration. A common option is to create multiple auto scaling groups and to route traffic to the nearest server with Geo DNS.
But with serverless technology, you now have another option for implementing cookie syncing, that includes using a Lambda function that executes in CloudFront edge locations.

By using Lambda@Edge serverless architecture instead of EC2 instances to manage the cookie-syncing process, businesses can benefit from the following advantages:

  • High cookie sync success rates because clients located across the globe will receive a response from the closest CloudFront edge before they navigate to another page
  • Lower maintenance costs due to centralized configuration and deployment to all locations, compared with manually configuring servers in each data center
  • Quick setup with an integrated service – developers just provide the code and don’t need to configure scaling, deployment to multiple locations or monitoring metrics

In this blog post, we explain how advertising companies can use Lambda@Edge to achieve these advantages when they implement a cookie-syncing process.

How cookie syncing works

To help illustrate the process and explain the technical aspects, we’ll use the following two components: a demand-side platform (DSP) and a data management platform (DMP).

  • A DSP is an AdTech platform that allows advertisers, including advertisers at a brand or an advertising agency, to buy ad impressions programmatically in real time, from ad exchanges.
  • A DMP is an AdTech platform used by many companies within the digital advertising ecosystem to import and manage user information, and create segments. Advertisers, publishers, and other AdTech companies all use DMPs to import data from various sources, create user profiles containing valuable attributes (such as age, location, interests, web browsing history, and more), and build audience segments which can be used to improve advertising ROI, target users, and increase revenue from advertising space.

Now let’s imagine the following scenario: An advertiser who operates a DSP wants to use the user profiles from the DMP to enhance their targeting capabilities and increase the potential value of each impression.

To do this, the advertiser creates an audience segment in the DMP—for example, the following:

Audience Segment #1

Campaign: Automotive sales ads

This audience segment contains the following attributes:

  • Male
  • Age 30-45
  • Lives in the U.S.
  • Has browsed used car sites at least twice in the past month

All of the user profiles that contain the attributes listed in this audience segment are retrieved and exported to the DSP. However, because cookies are domain specific, the DSP and DMP have different cookies set for the same device, under their respective domains:

  • The DMP stores a user identifier in a cookie which is stored under the DMP’s domain – Cookie: dmp_uid=5f5dad31b Domain:
  • The DSP also stores an identifier for the same device, but in a cookie under their domain – Cookie: dsp_uid=9f72dd4c Domain:

In order for the advertiser to identify the users that belong to the desired audience segment so they can be targeted for the right ads, the DSP and DMP must perform cookie syncing.

How user IDs are synced and mapped between AdTech platforms

The following diagram illustrates how the DSP’s user IDs and the DMP’s user IDs can be synced together and mapped.

The following describes how cookie syncing works, as shown in the diagram:

1. The user accesses a website that contains a code snippet from the DMP. The browser sends a request to load the DSP’s 1×1 transparent pixel called usersync.gif. The pixel is just used to assign to the user a third-party cookie containing an ID.

2. The DSP creates a new ID for the user. If the DSP’s cookie has already been set, the DSP just reads the ID from the cookie. Next, the DSP returns a redirect to the DMP’s cookie-syncing endpoint,, passing the ID (dsp_id=9f72dd4c) in the request parameters.

3. The browser follows the redirect.

4. The DMP receives the request, reads the dsp_id from the query string, and then saves it in its cookie-matching table, together with its own identifier.

5. The DMP returns a response to the browser, sending its ID in the cookie.

After completing the cookie-syncing process, the cookie-matching table stored in the DMP looks like the following:

theDMP ID SampleDSP ID
5f5dad31b 9f72dd4c

It’s important to note that while the processes in the browser— that is, requests and redirects between the browser, DSP, and DMP, and adding user IDs to the cookie-matching tables— happen in real time, the actual sharing of user data—that is, web history, recent purchases, and so on—occurs at a different time. For example, sharing user data might happen once a day at a certain time.

Now that cookie syncing is set up, whenever the DSP and DMP want to exchange information—for example, so they can identify the users that belong to the audience segment—they just need to reference the DSP’s IDs.

Creating a sample cookie syncing endpoint with Lambda@Edge

Now that we’ve seen how cookie sharing works, let’s walk through a short tutorial about how to implement it by using Lambda@Edge. We’ll focus on setting up the Demand-Side Platform’s cookie-syncing endpoint, which is fairly simple. We just need to read and set cookies, and then return a redirect.

Now that we’ve seen how cookie sharing works, let’s walk through a short tutorial about how to implement it by using Lambda@Edge. We’ll focus on setting up the Demand-Side Platform’s cookie-syncing endpoint, which is fairly simple. We just need to read and set cookies, and then return a redirect.

Choosing the Lambda trigger

We’ll host the DSP cookie-syncing endpoint on CloudFront and generate responses with Lambda code running on the edge.

If you’re not familiar with Lambda@Edge, here’s a brief overview of how it works. Requests sent to CloudFront can be processed by Lambda functions that can be set up to run in one or more of the following four phases:

  1. After CloudFront receives a request from a viewer (viewer request) If the response can be served from cache, then the viewer response is served, and phases 2 and 3 are omitted.
  2. Before CloudFront forwards the request to the origin server (origin request).
  3. After CloudFront receives the response from the origin (origin response).
  4. Before CloudFront forwards the response to the viewer (viewer response).

A Lambda function developer can choose to trigger the function to run during any of the four phases. We’ll set up the cookie syncing function to be triggered on an Origin request. By triggering on origin requests, we take advantage of response caching in CloudFront. This way the Lambda function is only executed when there is no response already present in the CloudFront cache.

You can learn more about Lambda@Edge by checking out Customizing with Lambda@Edge in the CloudFront Developer Guide.

Configuring a CloudFront distribution

To get started, create an empty S3 bucket to use as the origin for your CloudFront distribution. Then, create a CloudFront web distribution by doing the following:

  1. In the AWS Management Console, open the CloudFront console at
  2. Choose Create Distribution.
  3. In the Web section, choose Get Started.

When you configure the distribution, make the following selections:

  1. For the origin, choose your empty S3 bucket.
  2. For Viewer Protocol Policy, we recommend that you choose Redirect HTTP to HTTPS or HTTPS only to help protect the security and confidentiality of communication.
  3. For the other basic distribution settings, you can leave the default options.

Distribution origin and cache settings

In order to share the cookies across the DSP’s multiple services, the content must be served from the DSP’s domain. Before you can complete the next section for configuring your distribution, complete the following steps:

  1. Create a subdomain for cookie syncing, for example,, and configure a CNAME DNS entry for this subdomain that points to the Cloudfront domain. For detailed steps, see the following:
  2. If you don’t have an SSL certificate for the domain already stored in the AWS Certificate Manager, upload your certificate to ACM, or create a new one by following these steps:

Next, update the Distribution Settings section for your distribution by doing the following:

  1. For Alternate Domain Names (CNAMEs), enter the subdomain that you created.
  2. Select Custom SSL Certificate, and then, in the dropdown list, choose the certificate for your domain.
  3. Choose Create Distribution.


Distribution domain configuration

Tip: Make a note of the distribution ID for your distribution. You’ll need it later when you create a trigger for your Lambda function.

Creating a stub Lambda function

Now that you’ve set up your CloudFront distribution, create and configure a simple Lambda function to generate a response from CloudFront.

Tip: If you’re just getting started with Lambda@Edge, you might find it helpful to review the Lambda@Edge tutorial in the CloudFront Developer Guide.

To create the Lambda Function and add a CloudFront trigger for it, follow these steps:

1. In the AWS Management Console, open the Lambda console at

2. Choose Create function, and then choose Author from scratch to create a new function.

Creating a Lambda function

3. Complete the Basic information section to name your function and set a policy to provide permissions for CloudFront to run the function for you. Detailed steps are provided in the Lambda@Edge tutorial found in the Developer Guide.

4. In the Configure trigger section, for Distribution ID, enter the ID for the CloudFront distribution that you created.

5. For CloudFront Event, choose Origin Request.

6. Select Enable trigger and replicate. This option enables CloudFront to replicate your function to edge locations.

Lambda trigger configuration

7. For Lambda function code, copy and paste the following code into the editor.

This sample function simply returns an HTTP response with the status code 200.

Stub Lambda function body
'use strict';

exports.handler = (event, context, callback) => {
     * Generate HTTP response using 200 status code with a simple body.
    const response = {
        status: '200',
        statusDescription: 'OK',
        body: 'Example body generated by Lambda@Edge function.'

    callback(null, response);

8. In the Advanced settings section, set Timeout to 3s. This is the highest acceptable timeout for this application.

9. Choose Save.

Note that because we configured the trigger with Cache Behavior (*), which matches all request paths, a request with any content path will trigger the Lambda function.

Testing the CloudFront and Lambda function setup

Before you can test your function, CloudFront must finish deploying it by replicating it globally. In the CloudFront console, check the Status for your distribution. Wait until the status changes from In Progress to Deployed. You may need to wait several minutes.

Distribution status

After the function finishes replicating, you can test your CloudFront setup and response generation.

To test, access the URL for any content path in your distribution. For example, type from any web browser. Or enter the URL with the curl command line tool, as you can see in the following example:

Test of Lambda function stub
$ curl -v ''
*   Trying
* Connected to ( port 443 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate:
* Server certificate: Amazon
* Server certificate: Amazon Root CA 1
* Server certificate: Starfield Services Root Certificate Authority - G2
> GET /test HTTP/1.1
> Host:
> User-Agent: curl/7.54.0
> Accept: */*
< HTTP/1.1 200 OK
< Content-Length: 47
< Connection: keep-alive
< Server: CloudFront
< Date: Wed, 30 Aug 2017 12:51:17 GMT
< Last-Modified: 2017-01-13
< Vary: *
< X-Cache: LambdaGeneratedResponse from cloudfront
< Via: 1.1 (CloudFront)
< X-Amz-Cf-Id: H8Op6uYwXz87_z8GfjjfmMjhS1XL19qz_SiPZv4u-ZhsCBJM-CKjzQ==
* Connection #0 to host left intact
Example body generated by Lambda@Edge function

When you access the URL, you should see the following response returned, that we coded in the Lambda function: Example body generated by Lambda@Edge function.

Implementing the complete cookie sync process

After you’ve tested to make sure that the CloudFront and Lambda configuration is working, the next step is to develop a full implementation of the DSP’s cookie-syncing endpoint.

Creating a cookie-sync function

To create a cookie-sync Lambda function, begin by creating a new Lambda function using the code provided below. This time, do not specify a trigger. We’ll add a trigger later after we test the Lambda function.

A fragment of the cookie sync function. Complete function code is available on GitHub
const uuidv4 = require('uuid/v4'),
    prefix = /^\/getuid\//,
    macro = /\$UID/,
    dspCookie = 'dsp_uid',
    domain = '';

exports.handler = (event, context, responseCallback) => {
    const request = event.Records[0].cf.request,
        requestURI = request.uri;

    if (!(prefix.test(requestURI) && macro.test(requestURI))) {
        responseCallback(null, {
            status: '400',
            statusDescription: 'Invalid Request'

    let receivedDspUID = getUidValue(request.headers.cookie),
        dspUID = receivedDspUID || uuidv4(),
        redirectAddress = requestURI.replace(prefix, '').replace(macro, dspUID);

    responseCallback(null, {
        status: '301',
        headers: {
            location: [ {
                key: 'Location',
                value: redirectAddress
            } ],
            'set-cookie': [ {
                key: 'Set-Cookie',
                value: buildUidCookie(dspUID, 90)
            } ],

This function does the following:

  • It requires the request URL to have the following format: https://[CDN_URL]/getuid/[REDIRECT_URL], and requires that the redirect URL contains $UID macro text.
  • If the URL format is incorrect, it returns an error. An example of a correct URL is:$UID
  • It reads the dsp_uid cookie. If a cookie is not present, a new user ID is generated.
  • It strips the redirect address from the request URL, and then replaces the $UID macro with the user’s ID.
  • It returns a 301 response with the redirect URL and places the user identifier in the dsp_uid cookie.

Testing the function in the Lambda console

Before you add a trigger for the function, we recommend that you test it in the Lambda console. You can use the Lambda test event provided here to easily do that.

After you enter the function code, on the Actions tab, select Configure test event.

Test event configuration

Now copy and paste the following Lambda test event.

Lambda test event
  "Records": [
      "cf": {
        "request": {
          "headers": {
            "host": [
                "value": "",
                "key": "Host"
            "user-agent": [
                "value": "test-agent",
                "key": "User-Agent"
          "clientIp": "2001:cdba::3257:9652",
          "uri": "/getuid/$UID",
          "method": "GET"
        "config": {
          "distributionId": "EXAMPLE"

After you run the function, you should get a response of “301 moved permanently” and the location should contain a user ID, similar to the following example.

Lambda test event response
  "status": "301",
  "headers": {
    "location": [
        "key": "Location",
        "value": ""
    "set-cookie": [
        "key": "Set-Cookie",
        "value": "dsp_uid=de367df4-df0f-4917-b8f8-29d97377fee5; path=/;; expires=Wed, 29 Nov 2017 13:38:39 GMT; Secure"

Creating a new CloudFront cache behavior

After you’ve tested the function, create a new CloudFront cache behavior to add a trigger for the Lambda function and to configure CloudFront to work with the function correctly.

  1. In the CloudFront console, choose your distribution.
  2. Choose Distribution Settings.
  3. On the Behaviors tab, choose Create Behavior to add a behavior for the cookie-syncing path.
  4. For Path Pattern, enter a pattern that matches the beginning of your cookie-syncing path, for example, getuid/*.
  5. For Viewer Protocol Policy, select the Redirect HTTP to HTTPS option, just as you did in the earlier CloudFront setup.
  6. For Forward Cookies, choose Whitelist and add dsp_uid to the Whitelist Cookies list. It’s important that you enable cookie forwarding for the cookie syncing to work correctly.
  7. For Lambda Function Association, for Event Type, choose Origin Request and then enter the full ARN of your Lambda function, including the version. For example, enter arn:aws:lambda:us-east-1:759192338696:function:cookie_sync_redirect:14.


Cookie forwarding settings and Lambda association in distribution behavior settings

Save your changes, and then wait for the distribution status to change back to Deployed.

The reason we created a separate CloudFront behavior for cookie syncing is so that only the requests that match the cookie-syncing path are passed to the Lambda function. This helps you avoid unnecessary costs and allows you to configure multiple functions with the same CloudFront distribution. Now that we’ve added this specific behavior for cookie syncing, you can edit the Default (*) behavior for your distribution to remove the Lambda trigger added earlier.

Testing the cookie sync endpoint

You can now test your cookie sync endpoint from a browser or from the command line. For example, you can use the following commands, together with your distribution domain, to test that your solution works as expected.

Situation 1: User has no cookie set yet

When no cookies are passed with the request, the endpoint should return a cookie with a random user identifier and replace the $UID macro with the identifier.

Test endpoint without cookie (fragments omitted)
$ curl -v '$UID'
> GET /getuid/$UID HTTP/1.1
> Host:
< HTTP/1.1 301
< Location:
< Set-Cookie: dsp_uid=93fcefd8-d5ea-40e8-9eb1-dca37e850cc5; path=/;; expires=Tue, 28 Nov 2017 13:01:05 GMT; Secure

Situation 2: User is identified with a cookie

When the user has already been identified—that is, already has a cookie—the endpoint should return the same cookie, prolonging the cookie’s lifetime.

Test the endpoint with cookies (fragments omitted)
$ curl -v '$UID' \
     -H 'Cookie: dsp_uid=93fcefd8-d5ea-40e8-9eb1-dca37e850cc5;';

> GET /getuid/$UID HTTP/1.1
> Host:
> Cookie: dsp_uid=93fcefd8-d5ea-40e8-9eb1-dca37e850cc5;
< HTTP/1.1 301
< Location:
< Set-Cookie: dsp_uid=93fcefd8-d5ea-40e8-9eb1-dca37e850cc5; path=/;; expires=Tue, 28 Nov 2017 13:05:14 GMT; Secure

Optimizing costs by caching cookies

By using HTTP caching, you can limit the number of CloudFront origin requests and the number of Lambda function executions, reducing your costs.

When a user browses the internet, it is common for them to view a number of subpages during a visit to a specific site. Typically, each subpage view triggers a cookie-sync request. However, after you set the user ID cookie for your site, the cookie doesn’t change until the user clears their cookies or the cookie expires. Since there’s already a cookie in place, there’s no need to repeat the request.

Managing cookie-based caching

To lower costs, you should cache the response for the duration of each user’s visit. However, it’s important to note that not all request/response pairs should be cached in CloudFront. You’ll set the header Cache-Control to different values to manage correct caching.

For example, consider the following requests from a specific user’s browser:

# Request Response
No cookies
Set-Cookie: f3b0bed65…
Cache-Control: no-cache
Cookies: dsp_uid=57ba389f…
Set-Cookie: 57ba389f…
Cache-Control: max-age=3600

The browser making the first request is unidentified—that is, it doesn’t have a cookie set.

For each unidentified browser, the server should return a different cookie, so the response must not be cached in CloudFront. For this scenario, you set the response header Cache-Control: no-cache in the Lambda response object so the response is never cached.

The second request comes back with a dsp_uid cookie. For a given request path and cookie contents, the response will always be the same, so the response can be cached. This time, you set the response header Cache-Control: max-age=3600 to cause the response to be cached in CloudFront for up to 1 hour (3600 seconds).

Note: For this to work properly, be sure that you have enabled cookie-based caching in CloudFront, as described earlier. To enable cookie-based caching, edit the getuid/* behavior, set a HTTP origin and enable forwarding of the dsp_uid cookie.

The following code manages setting the Cache-Control header correctly for the two scenarios.

The code fragment responsible for setting Cache-Control headers
const cacheMaxAge = 60 * 60;  // in seconds, 1 hour = 60 min * 60 seconds

let receivedDspUID = getUidValue(request.headers.cookie),

if (!receivedDspUID) {
    // Do not cache the response when the browser doesn't have a cookie set
    // Caching the response in CDN would cause all new users to receive the same dspID
    cacheControl = [ {
        key: 'Cache-Control',
        value: 'no-cache'
    } ];
} else {
    // Cache the response based on cookie
    cacheControl = [ {
        key: 'Cache-Control',
        value: 'max-age=' + cacheMaxAge
    } ];

responseCallback(null, {
    status: '301',
    headers: {
        location: [ {
            key: 'Location',
            value: redirectAddress
        } ],
        'set-cookie': [ {
            key: 'Set-Cookie',
            value: buildUidCookie(dspUID, 90)
        } ],
        'cache-control': cacheControl

Storing the cookie-sync response in the browser cache

Depending on the response code returned from the server, the cookie-sync response can also be stored in the browser, lowering the number of requests to the server.

  • Responses with the status code 302, designed for temporary redirects, will not be cached.
  • Responses with the status code 301, representing a permanent redirect, will be cached. The lifespan (time before expiration) is determined by the Cache-Control header and other HTTP caching headers.

Results of a quick latency test

Content delivery networks, like CloudFront, are by definition distributed and designed for high request rates. With dynamic DNS routing and automatic scaling, it’s hard to conduct full load tests to estimate the maximum capacity of the service. However, it is possible to measure the response time and check the average for clients located at different distances from the content (origin) server.

We performed a latency test using wrk HTTP benchmarking tool with all client-side caching disabled to simulate requests from multiple devices. The test was run on a single machine, used two threads (each simulating five concurrent clients), and lasted for 10 minutes.

New visitor simulation – no server-side caching

The first test simulated traffic from first-time visitors that didn’t have any cookies yet. To achieve that, server-side caching was disabled, which caused the Lambda function to execute and set cookies for each incoming request.

As a base for comparison, a single instance running Nginx was deployed to US East (N. Virginia). Nginx was configured to perform the cookie syncing logic. Implementation of service logic on Nginx is not flexible, but because it relies on C code, it is expected to be very efficient.

In the test, the client was located in US West (N. California) and made requests to the instance running Nginx and Lambda@Edge. The Lambda function was deployed to all edge locations, including the edge closest to the client – US West (N. California).

Response time test results – new visitor, no caching
Server implementation
and location
Client location Average latency 90th percentile
CloudFront Lambda generated response,
deployed to all edge locations including
US West (N. California)
US West (N. California) 40.35ms 46.02ms
Nginx generated response, hosted in
US East (N. Virginia)
US West (N. California) 61.53ms 62.60ms

Lambda@Edge latency was on average 40.35 milliseconds, with 90th percentile at 46.02 milliseconds. Responses from Nginx in US East (N. Virginia) took, on average, 15ms more to reach the client, with 90% of responses received within 62.6ms.

The Nginx test server and the client were situated around 4500km apart. With the typical number of three data centers (US, Europe, Asia), it’s possible that clients will be located even further from the server, increasing distance-based latency.

Returning visitor simulation – server-side caching

A second test aimed to reproduce the requests from returning visitors. In this case, all responses were returned from CloudFront cache, omitting the invocation of the function.

The test measured the latency between CloudFront and a client located in US West (N. California). The closest edge location to the client is also in US West (N. California), so the server and the client were in the same region.

Response time test results – returning visitor, responses cached in CloudFront
Server implementation
and location
Client location Average latency 90th percentile
CloudFront cached response, deployed to all edge locations, including  US West (N. California) US West (N. California) 2.97ms 3.68ms


With server-side caching, the response was returned, on average, in under 3ms, with 90% of responses arriving in under 3.68ms.

Test summary

In real life, the traffic reaching the cookie syncing service is a mixture of new and returning visitor requests coming from various locations. Because of that, the actual average latency would be somewhere between the results for new and returning visitors. In the EC2 setup, the visitors experience additional latency due to distance to the server.

With round trip times lower than 100ms, both EC2-based and Lambda implementation met the latency requirements. It is very likely that cookie syncing will complete before a user navigates to another page, as human reaction time is typically more than 300ms.

Putting it into perspective: is Lambda@Edge the right choice for your scenario?

While the tests highlight the response time differences for Lambda@Edge and EC2-based cookie syncing, the keys to successful implementation go beyond just performance. You should also consider the following:

Reduced time to market and maintenance cost: A EC2-based configuration includes instances in multiple locations, load balancers, monitoring, and GeoDNS services.

Efficiency and scale: Because cookie-sync pixels in large AdTech platforms are triggered thousands of times per second, the chosen solution must be efficient and be able to scale easily, as is the case with Lambda@Edge

Global reach: Because AdTech platforms serve clients on multiple continents, it’s vital to ensure low latencies with multiple datacenters

Although Lambda@Edge is a relatively young service, it provides many advantages for a cookie-syncing implementation, and potentially for other AdTech platform functions. It’s worth considering and testing to see if it will meet the requirements of your specific AdTech service.

Also, because it’s easy to create and deploy Lambda@Edge functions, it’s a technology well suited to service proof-of-concept implementations. Just be aware of the differences around pricing, compared to EC2. For Lambda@Edge, you’re charged per request rather than per hour, as with EC2 instances.

This post has been prepared by Zuzanna Hartleb with contributions made by Maciej Zawadziński and Michael Sweeney from Clearcode, a full-service software house specializing in building custom advertising technology.

Blog: Using AWS Client VPN to securely access AWS and on-premises resources
Learn about AWS VPN services
Watch re:Invent 2019: Connectivity to AWS and hybrid AWS network architectures