DEV Community

Cover image for SnapStart for Python: measure and cut Lambda cold starts
Sascha Mirow-Stanitzok
Sascha Mirow-Stanitzok

Posted on Originally published at sascha-ms.de

SnapStart for Python: measure and cut Lambda cold starts

SnapStart for Python has been around for a while, yet in Python projects on Lambda I still see the same reflex when cold starts get slow: more memory, Provisioned Concurrency or a cron job that keeps the function “warm”. A built-in tool sits right next to those options. It needs one line of configuration and a bit of discipline in your code.

In this article you build a small Python function with AWS CDK, switch on SnapStart, measure the effect in CloudWatch and learn where SnapStart would otherwise quietly introduce bugs. By the end you will know whether the feature pays off for your functions and what it costs.

In short

SnapStart runs the init code of your Python function when you publish a version, freezes memory as a snapshot and starts new environments from that image. It pays off mainly with heavy imports and high load. The biggest pitfall: anything meant to be random or unique that you create in the init phase is identical in every environment after a restore.

What you will learn in this article

  • How SnapStart for Python works under the hood and when it pays off
  • Setting up SnapStart with AWS CDK, a version and an alias
  • Using runtime hooks from snapshot_restore_py correctly
  • Measuring cold starts before and after with Logs Insights
  • Avoiding typical mistakes with randomness, IDs, connections and costs

What is SnapStart for Python?

A cold start happens when Lambda needs a new execution environment: start the MicroVM, load the runtime, import your code and run everything at module level. In Python the last part is often the most expensive. If you import pandas, pydantic or a larger framework, you often wait much longer before the handler even runs.

SnapStart moves this work forward. As soon as you publish a function version, Lambda initializes your code once, takes a snapshot of the memory and disk of the Firecracker MicroVM, encrypts it and caches it. New environments then no longer start from zero; they are restored from this snapshot instead. According to the AWS documentation this works for Python 3.12 and later, Java 11 and later and .NET 8 and later. Node.js and Ruby are not included.

Diagram: without SnapStart, environment, runtime, imports and handler run; with SnapStart only restore, after_restore and handler

With SnapStart, the init phase moves to publishing the version.

The animated diagram shows the flow from deployment to the first invocation. Above all, the step in the middle matters: a snapshot always belongs to exactly one published version. New code means a new version and therefore a new snapshot.

🎬 Lifecycle of SnapStart for Python – this animated diagram only runs on my site: watch it on sascha-ms.de

When does SnapStart for Python pay off?

The honest answer: not always. SnapStart helps where the init phase dominates the cold start. A lean function that only loads json and boto3 gains little, because restoring and loading the runtime take time as well. A function that validates data models, compiles templates or loads a small model from S3 can benefit a lot. AWS also points out that SnapStart works best at scale; rarely invoked functions often do not see the same improvements.

Before you switch on SnapStart, it is therefore worth looking at the simple levers: fewer dependencies, late imports for rarely used paths, more memory (and therefore more CPU). For TypeScript I collected these levers in the guide to reducing Lambda cold starts, and most of them apply to Python just as well. I described how SnapStart works for Java in SnapStart for Spring Boot.

SnapStart or Provisioned Concurrency?

Both features fight cold starts, but in different ways. Provisioned Concurrency keeps a fixed number of environments ready all the time, while SnapStart makes every new environment faster. By the way, you cannot combine them.

Criterion SnapStart Provisioned Concurrency
Principle Restore from snapshot when scaling Pre-warmed environments, always ready
Cold start Shorter, but still there (restore) Practically none with enough reserve
Cost model Cache per version plus a fee per restore Hourly price per provisioned environment
Good for Fluctuating load, heavy init phase Predictable peaks, strict latency goals
Configuration One property plus version/alias Plan capacity, often on a schedule

If your load is predictable, scheduled Provisioned Concurrency can be the better choice. The article Scheduled Provisioned Concurrency shows how.

What SnapStart for Python costs

For Java, SnapStart comes at no extra charge. For Python and .NET, AWS bills two additional items: caching the snapshot per published version (based on allocated memory, with a minimum of three hours) and every restore from the snapshot. For example, the price list for US East (N. Virginia) shows USD 0.0000015046 per GB-second of cache and USD 0.0001397998 per GB restored. Other Regions can differ, so check the pricing page for your Region.

An example with my own assumptions: a function with 1 GB of memory, one active version, 30 days. The cache costs 2,592,000 GB-seconds × $0.0000015046 ≈ $3.90. With 20,000 restores per month you add 20,000 × 1 GB × $0.0001397998 ≈ $2.80. In total roughly $6.70 per month, on top of the normal cost for requests and duration. You can work those out easily with the Lambda cost calculator.

The real cost trap hides in the words “per version”. In fact, the cache is billed for every published version with SnapStart as long as it is active. The documentation describes automatic deletion after 14 days without invocations only for Java. So clean up old versions actively; more on that in the typical mistakes.

Prerequisites

  • An AWS account and the AWS CLI v2 with credentials for a test environment
  • Node.js 22 or later for AWS CDK and a one-time cdk bootstrap in the target Region
  • Python 3.12 or later locally for the tests (the function runs on python3.14)
  • A Region with SnapStart support; according to the documentation, all commercial Regions except Asia Pacific (New Zealand) and Asia Pacific (Taipei)

The example is deliberately small: a fictional city library that serves reading recommendations per genre and leaves out books a member has already borrowed. The catalog is a JSON file next to the code, the loans live in DynamoDB.

Step by step: SnapStart for Python with AWS CDK

1. Create the project

Create the folder structure and install the tools. For the tests you need boto3, pytest and the snapshot-restore-py library locally. The library already ships with the Lambda Python runtime, so you do not need to bundle it.

mkdir -p snapstart-python/{src/recommendations,tests,infra/bin,infra/lib}
cd snapstart-python
python3 -m venv .venv && source .venv/bin/activate
pip install boto3 pytest snapshot-restore-py
cd infra
npm init -y
npm install aws-cdk-lib constructs
npm install -D aws-cdk typescript tsx @types/node
Enter fullscreen mode Exit fullscreen mode

Also create the file src/recommendations/catalog.json with a few books. Each entry has isbn, title, genre and loans (number of loans). All titles are made up.

[
  {"isbn": "978-3-00-000001-1", "title": "Fog over the Harbour", "genre": "crime", "loans": 412},
  {"isbn": "978-3-00-000002-8", "title": "The Silent Witness", "genre": "crime", "loans": 377},
  {"isbn": "978-3-00-000003-5", "title": "Death in the Reading Room", "genre": "crime", "loans": 298},
  {"isbn": "978-3-00-000004-2", "title": "The Last Ferry", "genre": "crime", "loans": 251},
  {"isbn": "978-3-00-000005-9", "title": "Stars over Cassiopeia", "genre": "scifi", "loans": 389},
  {"isbn": "978-3-00-000006-6", "title": "Signal from Nowhere", "genre": "scifi", "loans": 344},
  {"isbn": "978-3-00-000007-3", "title": "The Colony at the Edge", "genre": "scifi", "loans": 205},
  {"isbn": "978-3-00-000008-0", "title": "Baking Bread for Beginners", "genre": "nonfiction", "loans": 466},
  {"isbn": "978-3-00-000009-7", "title": "Balcony Gardening", "genre": "nonfiction", "loans": 318},
  {"isbn": "978-3-00-000010-3", "title": "Fix Your Own Bike", "genre": "nonfiction", "loans": 290}
]
Enter fullscreen mode Exit fullscreen mode

2. The handler with runtime hooks

Now comes the most important part. Everything at module level runs in the init phase and ends up in the snapshot: the DynamoDB client, the loaded catalog and the precomputed genre index. That is exactly what we want. The ID of the execution environment, on the other hand, is only created after the restore; otherwise all environments would share it.

# src/recommendations/handler.py
"""Reading recommendations for a fictional city library – example for Lambda SnapStart with Python."""
import json
import os
import random
import time
import uuid
from pathlib import Path

import boto3
from snapshot_restore_py import register_after_restore, register_before_snapshot

MAX_LIMIT = 10

def build_genre_index(books: list[dict]) -> dict[str, list[dict]]:
    """Groups the catalog by genre, most popular titles first."""
    index: dict[str, list[dict]] = {}
    for book in sorted(books, key=lambda b: b["loans"], reverse=True):
        index.setdefault(book["genre"], []).append(book)
    return index

# --- Init phase: runs once when the version is published and ends up in the snapshot ---
dynamodb = boto3.resource("dynamodb")
loans_table = dynamodb.Table(os.environ.get("LOANS_TABLE", "library-loans"))

CATALOG = json.loads((Path(__file__).parent / "catalog.json").read_text(encoding="utf-8"))
GENRE_INDEX = build_genre_index(CATALOG)

# Secure randomness: reads OS entropy, which is fresh after a restore
secure_rng = random.SystemRandom()

# Unique per execution environment – so do NOT create it here in the init phase
environment_id: str | None = None
restored_at: float | None = None

def recommend(genre: str, limit: int) -> list[dict]:
    """Returns the most popular titles of a genre."""
    return [dict(book) for book in GENRE_INDEX.get(genre, [])[:limit]]

def loaned_isbns(member_id: str) -> set[str]:
    """Reads the ISBNs the member has already borrowed."""
    response = loans_table.query(
        KeyConditionExpression="memberId = :m",
        ExpressionAttributeValues={":m": member_id},
        ProjectionExpression="isbn",
    )
    return {item["isbn"] for item in response.get("Items", [])}

@register_before_snapshot
def warm_up() -> None:
    """Runs before the snapshot: exercise code paths once, without side effects."""
    for genre in GENRE_INDEX:
        json.dumps(recommend(genre, MAX_LIMIT), ensure_ascii=False)

@register_after_restore
def refresh_after_restore() -> None:
    """Runs after every restore: recreate everything that must be unique."""
    global environment_id, restored_at
    environment_id = str(uuid.uuid4())
    restored_at = time.time()

def parse_limit(raw: str | None) -> int:
    try:
        value = int(raw) if raw else 3
    except ValueError:
        value = 3
    return max(1, min(value, MAX_LIMIT))

def handler(event: dict, context) -> dict:
    global environment_id
    if environment_id is None:
        # Fallback for $LATEST without SnapStart (e.g. local tests)
        environment_id = str(uuid.uuid4())

    params = event.get("queryStringParameters") or {}
    genre = (params.get("genre") or "crime").lower()
    limit = parse_limit(params.get("limit"))
    member_id = params.get("member")

    already_read = loaned_isbns(member_id) if member_id else set()
    candidates = [b for b in recommend(genre, MAX_LIMIT) if b["isbn"] not in already_read][:limit]
    secure_rng.shuffle(candidates)

    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json; charset=utf-8"},
        "body": json.dumps(
            {"genre": genre, "items": candidates, "environmentId": environment_id},
            ensure_ascii=False,
        ),
    }
Enter fullscreen mode Exit fullscreen mode

Two decorators from snapshot_restore_py do the work. @register_before_snapshot runs once before Lambda takes the snapshot. Here, warm_up() runs through the recommendation logic so Python has executed all code paths once. The hook deliberately has no side effects: no database access, no order, no email. @register_after_restore runs after every restore and creates a fresh environment ID.

For randomness the handler uses random.SystemRandom(). This is a cryptographically secure generator that reads from operating system entropy. That entropy is fresh after a restore, whereas the state of the regular random module is stuck in the snapshot.

3. Write and run the tests

Because the decorators return the functions unchanged, you can call the hooks directly in tests. The DynamoDB access is replaced with unittest.mock, so the tests run without AWS access. The article Lambda unit tests with Vitest shows how to test Lambda handlers in general, using TypeScript.

# tests/test_handler.py
import json
import os
import sys
from pathlib import Path
from unittest.mock import patch

os.environ.setdefault("AWS_DEFAULT_REGION", "eu-central-1")
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src" / "recommendations"))

import handler  # noqa: E402

def call(params):
    return json.loads(handler.handler({"queryStringParameters": params}, None)["body"])

def test_recommends_most_popular_titles_of_a_genre():
    body = call({"genre": "Crime", "limit": "2"})
    assert body["genre"] == "crime"
    assert {b["title"] for b in body["items"]} == {"Fog over the Harbour", "The Silent Witness"}

def test_limit_is_clamped():
    assert len(call({"genre": "crime", "limit": "500"})["items"]) == 4
    assert len(call({"genre": "crime", "limit": "abc"})["items"]) == 3

def test_skips_books_the_member_has_already_borrowed():
    with patch.object(handler.loans_table, "query", return_value={"Items": [{"isbn": "978-3-00-000001-1"}]}):
        body = call({"genre": "crime", "limit": "2", "member": "M-1001"})
    assert "Fog over the Harbour" not in {b["title"] for b in body["items"]}
    assert len(body["items"]) == 2

def test_warm_up_has_no_side_effects():
    with patch.object(handler.loans_table, "query") as query:
        handler.warm_up()
    query.assert_not_called()

def test_after_restore_creates_a_new_environment_id():
    handler.refresh_after_restore()
    first = handler.environment_id
    handler.refresh_after_restore()
    assert handler.environment_id != first
    assert handler.restored_at is not None
Enter fullscreen mode Exit fullscreen mode
python -m pytest -q
Enter fullscreen mode Exit fullscreen mode

Expected result: 5 passed. If importing snapshot_restore_py fails, the library is missing from your virtual environment.

4. The stack with SnapStart and an alias

In the CDK stack, the property snapStart: lambda.SnapStartConf.ON_PUBLISHED_VERSIONS switches the feature on. That alone is not enough, though, because SnapStart only affects published versions. That is why currentVersion creates a new version on every change, and the live alias always points to it.

// infra/lib/recommendations-stack.ts
import * as path from 'node:path';
import { Annotations, CfnOutput, Duration, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as logs from 'aws-cdk-lib/aws-logs';
import { Construct } from 'constructs';

export class RecommendationsStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const loansTable = new dynamodb.TableV2(this, 'LoansTable', {
      partitionKey: { name: 'memberId', type: dynamodb.AttributeType.STRING },
      sortKey: { name: 'isbn', type: dynamodb.AttributeType.STRING },
      removalPolicy: RemovalPolicy.DESTROY, // for this example only
    });

    const recommendFn = new lambda.Function(this, 'RecommendationsFunction', {
      runtime: lambda.Runtime.PYTHON_3_14,
      handler: 'handler.handler',
      code: lambda.Code.fromAsset(path.join(__dirname, '../../src/recommendations')),
      memorySize: 1024,
      timeout: Duration.seconds(10),
      // SnapStart only applies to published versions
      snapStart: lambda.SnapStartConf.ON_PUBLISHED_VERSIONS,
      environment: { LOANS_TABLE: loansTable.tableName },
      logGroup: new logs.LogGroup(this, 'RecommendationsLogs', {
        retention: logs.RetentionDays.TWO_WEEKS,
        removalPolicy: RemovalPolicy.DESTROY,
      }),
    });
    loansTable.grantReadData(recommendFn);

    // The warning “SnapStart only supports published versions” is handled here:
    // currentVersion publishes a new version on every change.
    Annotations.of(recommendFn).acknowledgeWarning(
      '@aws-cdk/aws-lambda:snapStartRequirePublish',
      'Version is published via currentVersion and the live alias',
    );

    // The alias always points to the latest published version.
    // Callers (API Gateway, EventBridge, CLI) must use the alias, not $LATEST.
    const liveAlias = new lambda.Alias(this, 'LiveAlias', {
      aliasName: 'live',
      version: recommendFn.currentVersion,
    });

    new CfnOutput(this, 'FunctionName', { value: recommendFn.functionName });
    new CfnOutput(this, 'AliasArn', { value: liveAlias.functionArn });
  }
}
Enter fullscreen mode Exit fullscreen mode
// infra/bin/app.ts
import { App } from 'aws-cdk-lib';
import { RecommendationsStack } from '../lib/recommendations-stack';

const app = new App();
new RecommendationsStack(app, 'LibraryRecommendations', {
  env: { region: process.env.CDK_DEFAULT_REGION ?? 'eu-central-1' },
});
Enter fullscreen mode Exit fullscreen mode

The cdk.json only needs the start command: {"app": "npx tsx bin/app.ts"}. Without the acknowledgeWarning call, CDK reports the warning snapStartRequirePublish. It reminds you that no snapshot is created without a version. Only acknowledge it once you really publish a version.

5. Deploy and check the snapshot

cd infra
npx tsc --noEmit
npx cdk deploy
Enter fullscreen mode Exit fullscreen mode

The deployment takes a little longer than usual because Lambda runs your init code and creates the snapshot when the version is published. Afterwards, check the state of the version behind the alias. Replace the function name with the value of the stack output FunctionName.

aws lambda get-function-configuration \
  --function-name <FunctionName>:live \
  --query '{SnapStart: SnapStart, State: State, Version: Version}'
Enter fullscreen mode Exit fullscreen mode

You should see "ApplyOn": "PublishedVersions", "OptimizationStatus": "On" and "State": "Active". If it says Failed, the init phase or a before_snapshot hook failed; you will find the cause in the function log.

6. Invoke the function through the alias

aws lambda invoke \
  --cli-binary-format raw-in-base64-out \
  --function-name <FunctionName>:live \
  --payload '{"queryStringParameters": {"genre": "scifi", "limit": "2"}}' \
  response.json
cat response.json
Enter fullscreen mode Exit fullscreen mode

As a result, you get two science fiction titles and an environmentId. If you invoke the function in parallel so that Lambda starts several environments, each environment gets its own ID. That proves the after_restore hook works.

Measuring cold starts with SnapStart for Python

Without measurement, every optimization is a gut feeling. With SnapStart, Lambda writes two extra fields into the REPORT line of a new environment: Restore Duration (restoring the snapshot, loading the runtime, after_restore hooks) and Billed Restore Duration. The classic Init Duration is then missing from the REPORT; it appears in a separate INIT_REPORT entry from the time the snapshot was created.

Use this Logs Insights query to compare cold starts before and after. It only counts invocations that needed a new environment and adds init or restore time to the handler duration.

filter @type = "REPORT"
| parse @message /Restore Duration: (?<restoreMs>[0-9.]+) ms/
| filter ispresent(@initDuration) or ispresent(restoreMs)
| fields @duration + coalesce(@initDuration, 0) + coalesce(restoreMs, 0) as coldStartMs
| stats count(*) as coldStarts,
        pct(coldStartMs, 50) as p50,
        pct(coldStartMs, 90) as p90,
        pct(coldStartMs, 99) as p99
Enter fullscreen mode Exit fullscreen mode

My suggestion for a fair comparison: deploy the stack without snapStart first, create a few dozen cold starts with a small load test and note p50 and p99. Then switch on SnapStart, deploy again and repeat the same test. For single invocations, AWS X-Ray additionally shows a Restore subsegment instead of Initialization.

Using runtime hooks in Python correctly

The hooks are quick to add, but they follow a few rules you should know. In addition, the library is included in the managed Python runtimes. If your hooks live in a separate module, the handler file must import that module; otherwise Lambda ignores them.

  • Order: before_snapshot hooks run in reverse order of registration, after_restore hooks in order of registration.
  • Time limit before the snapshot: init code and before_snapshot hooks may take up to 130 seconds or the function timeout together, whichever is higher (at most 15 minutes).
  • Time limit after the restore: loading the runtime and all after_restore hooks must finish within 10 seconds, otherwise you get a SnapStartTimeoutException.
  • Files in /tmp: anything a before_snapshot hook writes to /tmp becomes part of the snapshot. That is useful for models or templates you would otherwise download on every cold start.

The functions also exist without the decorator syntax: register_before_snapshot(func, *args, **kwargs). This helps when you want to control the order explicitly or pass arguments.

Typical mistakes with SnapStart for Python

Invoking the function through $LATEST

SnapStart does not work with $LATEST. If API Gateway, EventBridge or another caller uses the unqualified function ARN, everything runs as before, just without a snapshot. So check that every integration points to the alias.

Creating randomness and IDs in the init phase

This is the most dangerous mistake because it throws no exception. In other words, anything created in the init phase exists identically in every environment after a restore. With tokens, nonces or keys this quickly turns into a security problem.

# ❌ Don't: runs in the init phase and ends up in the snapshot
import random
import uuid

random.seed()                     # state gets frozen
REQUEST_PREFIX = uuid.uuid4().hex  # identical in every environment

def handler(event, context):
    token = random.randint(100000, 999999)  # repeats after every restore
    ...
Enter fullscreen mode Exit fullscreen mode
# ✅ Better: create unique values only after the restore
import random
import secrets

from snapshot_restore_py import register_after_restore

secure_rng = random.SystemRandom()  # uses operating system entropy
request_prefix = None

@register_after_restore
def new_identity():
    global request_prefix
    request_prefix = secrets.token_hex(8)

def handler(event, context):
    token = secure_rng.randint(100000, 999999)
    ...
Enter fullscreen mode Exit fullscreen mode

socket.gethostname() is similarly deceptive: every environment that comes from the same snapshot reports an identical hostname. For logs or metrics per environment, create the ID yourself, as in the example above.

Reusing connections outside the AWS SDK

Connections you open in the init phase are not guaranteed to survive the restore. The AWS SDK, however, usually resumes its connections on its own. Check your own connections to databases, Redis or HTTP pools in the handler, or rebuild them in the after_restore hook. Binding to fixed local ports is not a good idea either.

Forgetting old versions

In practice, every deployment with changed code creates a new version and therefore a new snapshot with its own cache cost. After a few weeks with many deployments, this adds up. AWS recommends finding unused versions with ListVersionsByFunction and removing them with DeleteFunction. However, keep the version your alias points to, and perhaps one for rollbacks.

aws lambda list-versions-by-function \
  --function-name <FunctionName> \
  --query "Versions[?Version!='\$LATEST'].Version"

aws lambda delete-function --function-name <FunctionName>:3
Enter fullscreen mode Exit fullscreen mode

Unsupported combinations

You cannot combine SnapStart with Provisioned Concurrency, Amazon EFS or more than 512 MB of ephemeral storage in /tmp. CDK already catches EFS and too much ephemeral storage at synth time and stops with an error message. That is convenient because you see the problem before deploying.

My take

What I like about SnapStart is that it tackles cold starts with a configuration line instead of permanently paid reserves. For Python functions with heavy imports that is a real gain, and the runtime hooks are simple enough for a team without SnapStart experience to understand.

I see two things critically. First, SnapStart shifts bugs from “slow” to “wrong”: a reused random value does not show up in tests, only in production. That is why the code review question “What happens here in the init phase?” is a fixed part of the review for me. Second, SnapStart for Python is not free as it is for Java. If you publish many versions and never clean up, you pay for snapshots nobody uses. My advice: measure first, then switch it on, and add the cleanup of old versions to your pipeline right away.

FAQ

Which Python versions does SnapStart support?

SnapStart works with the managed runtimes from Python 3.12 onwards, so currently python3.12, python3.13 and python3.14. Container images are supported as well if they are based on the AWS base images or implement the hooks themselves.

Do I need to bundle snapshot-restore-py with my deployment?

No. Lambda already ships the library with its Python runtimes. You only install it locally so that tests and your editor can resolve the import.

Is SnapStart available for Node.js or TypeScript?

No. According to the AWS documentation, other managed runtimes such as Node.js and Ruby are not supported. For Node.js, lean bundles, more memory or Provisioned Concurrency remain your options.

Can I use SnapStart and Provisioned Concurrency together?

That combination is not possible. You choose one of the two approaches per function.

How long does a restore take in Python?

In general, that depends on memory size, code and hooks. You only get reliable numbers by measuring yourself, for example with the Logs Insights query from this article.

Because every published version with SnapStart adds caching cost, old versions deserve a look. I show how to get rid of them automatically in Clean Up Lambda Versions: Prune and S3 Reference Mode.

To see what actually happens inside the function after switching to SnapStart, Lambda Powertools for Python: logs, traces, metrics shows how to add structured logs and a cold start metric.

Your next step

Pick your slowest Python function and measure p50 and p99 of its cold starts for a week. Then switch on SnapStart in a test environment, check every line at module level with the question “May this be the same in all environments?” and compare. I am curious: which Python library causes the longest cold starts for you?

Before you measure SnapStart, make sure the build is clean: how to build Python dependencies with uv as an arm64 layer is explained in uv for AWS Lambda.

Sources

This post was created with AI assistance.


👉 Next step: Curious what your own Lambda functions cost? Try my Lambda cost calculator – it runs right in your browser.

📬 Stay updated: New articles on serverless, AI and leading tech teams via RSS, browser push or the weekly newsletter (in German).

Originally published on sascha-ms.de. More articles on serverless, AI in everyday development and leading tech teams: sascha-ms.de/en

Top comments (0)