zerolith.ioVérification…

← Tous les articles

Tutoriel · Cron · Facturation

Un cron serverless qui ne coûte rien entre deux exécutions

6 min de lecture

Une tâche planifiée n'a pas besoin d'un serveur allumé 24 h sur 24 pour tourner une fois par jour. On déploie une fonction, on lui accroche un cron, et on regarde ce que ça coûte vraiment : les secondes d'exécution, et rien d'autre.

Il y a une catégorie de tâche que presque tout le monde héberge mal : le job qui tourne une fois par jour. Un rapport à envoyer le matin, une API tierce à interroger, un cache à réchauffer, une sauvegarde à déclencher. Le travail dure deux secondes ; la machine qui l'exécute, elle, tourne 86 400 secondes par jour.

Le rapport entre ce qu'on paie et ce qu'on utilise est donc de l'ordre de 1 à 43 000. Voici comment déployer le même job sous forme de fonction planifiée, et surtout ce que ça coûte réellement, arithmétique comprise, y compris la part que la plupart des articles sur le serverless oublient de mentionner.

La fonction

Une fonction Zerolith est un handler. Pas de framework à instancier, pas de serveur à démarrer : une fonction Python qui reçoit une requête et renvoie ce qu'elle veut.

Le job d'exemple surveille une page et signale si son contenu a changé de forme, le genre de vérification qu'on veut quotidienne sans avoir à y penser. requests est déjà dans l'image du runtime, comme httpx, pydantic, PyYAML et le client de base de données. Rien à installer, rien à construire.

main.py
import os
import requests

WATCHED = os.environ.get("WATCHED_URL", "https://example.org/")
NOTIFY = os.environ.get("NOTIFY_WEBHOOK")  # Slack, Discord, Mattermost…


def handler(request):
    r = requests.get(WATCHED, timeout=10)
    ok = r.status_code == 200 and "En savoir plus" in r.text

    if not ok and NOTIFY:
        requests.post(NOTIFY, json={"text": f"{WATCHED} a changé (HTTP {r.status_code})"},
                      timeout=10)

    # Un dict est renvoyé tel quel en JSON, avec un 200.
    return {"url": WATCHED, "status": r.status_code, "ok": ok}

Le contrat complet du handler (request.method, request.headers, request.query, request.json(), et les formes de retour acceptées : dict, (status, body), (status, body, headers)) est dans la documentation.

L'URL surveillée et l'URL du webhook sont des variables d'environnement plutôt que des constantes dans le code. Le webhook est un secret, et un secret créé en tant que tel n'est jamais relisible, ni dans l'interface ni par l'API : il est monté dans le pod depuis un objet en écriture seule.

créer le secret
# La valeur part vers le cluster et ne revient jamais.
curl -X POST -H "Authorization: Bearer $JWT" \
     -d '{"name": "NOTIFY_WEBHOOK", "kind": "secret", "value": "https://hooks.slack.com/…"}' \
     https://zerolith.io/api/env-vars

Le déploiement

Une fonction se déploie en un appel. size vaut small par défaut, soit 128 Mio de mémoire et 0,1 vCPU réservés, ce qui est large pour un appel HTTP et une comparaison de chaînes.

déployer
curl -X POST -H "Authorization: Bearer $JWT" \
     -d '{"name": "veille-quotidienne",
          "language": "python",
          "size": "small",
          "code": "…contenu de main.py…",
          "env_var_ids": ["<id-du-secret>"]}' \
     https://zerolith.io/api/functions

À ce stade la fonction est joignable en HTTP authentifié, et aucun pod ne tourne. C'est l'état de départ, pas une optimisation : min_scale vaut 0, donc tant que personne n'appelle, il n'y a rien à facturer.

Le cron

Le déclencheur est une expression cron standard à cinq champs, en UTC, attachée à la fonction :

planifier
# Tous les jours à 7 h UTC.
curl -X POST -H "Authorization: Bearer $JWT" \
     -d '{"cron": "0 7 * * *"}' \
     https://zerolith.io/api/functions/<id>/schedules

La réponse contient next_run_at, l'instant exact du prochain déclenchement, calculé côté plateforme. Après le premier passage, la même ressource porte last_run_at et last_status, un résumé compact du dernier tir ("200", "skipped: no credit", "error: timeout"). C'est ce champ qu'il faut regarder quand un cron « ne s'est pas exécuté » : la plupart du temps il s'est exécuté et il a échoué, ce qui n'est pas le même problème.

Un PATCH sur /api/schedules/<id> avec {"enabled": false} suspend le déclencheur sans détruire la fonction. La limite est de 100 planifications par compte.

Deux pièges classiques, parce qu'ils ne se déduisent pas :

  • UTC, sans exception. Pas de fuseau, pas d'heure d'été. 0 7 * * * tombe à 8 h ou 9 h en France selon la saison. Si l'heure locale compte pour vous, choisissez laquelle des deux vous voulez et assumez le décalage de l'autre.
  • Le dispatcher abandonne un tir au bout de 30 secondes, indépendamment du timeout_seconds de la fonction (jusqu'à 600 s, 120 par défaut). Un cron n'est donc pas l'outil pour un traitement de dix minutes ; il est l'outil pour démarrer ce traitement.

Ce que ça coûte, vraiment

Les prix sont publics et unitaires : 0,000004 € par Gio-seconde de mémoire, 0,0000125 € par vCPU-seconde, 0,0000004 € par invocation. La taille small réserve 0,125 Gio et 0,1 vCPU, ce qui donne, par seconde de pod en vie :

coût par seconde
mémoire   0,125 Gio × 0,000004 €  =  0,0000005  €/s
processeur  0,1 vCPU × 0,0000125 € =  0,00000125 €/s
                                     ────────────────
                                      0,00000175 €/s

Vient maintenant la partie que je n'ai vue écrite nulle part : ce ne sont pas deux secondes que vous payez.

Un pod ne disparaît pas à la milliseconde où la réponse part. L'autoscaler de Knative observe une fenêtre de stabilité, 60 secondes par défaut et réglable par fonction, puis laisse encore un délai de grâce avant de descendre à zéro. Pour un tir de deux secondes, comptez donc de l'ordre d'une minute et demie de pod facturé. C'est la vraie unité de facturation d'un cron, et c'est ce chiffre qu'il faut utiliser :

un cron quotidien, sur un mois
par tir     ≈ 90 s × 0,00000175 €  ≈ 0,000158 €   (+ 0,0000004 € d'invocation)
30 tirs     ≈ 0,0047 €

Un demi-centime par mois. Le crédit d'un euro offert à l'inscription couvre ce cron pendant plus de dix ans.

La comparaison honnête n'est pas « serverless contre VPS » dans l'absolu : un VPS à 5 €/mois vous donne une machine persistante, un disque, un état, des voisins que vous choisissez. Ce que dit le calcul, c'est que pour ce cas précis, un job sans état, quelques secondes, quelques fois par jour, vous payiez un facteur mille de trop, et que ce facteur mille venait entièrement du temps où il ne se passait rien.

Vous pouvez refaire l'addition sur votre propre compte : GET /api/usage/summary renvoie les requêtes facturées, les Gio-secondes et le coût du mois en cours, et GET /api/usage/windows?function_id=<id> détaille fenêtre par fenêtre.

Ce qui reste vrai quand ça se passe mal

Un cron est un appel comme un autre, et il traverse exactement les mêmes contrôles :

  • Crédit à zéro, rien ne se réveille. Le contrôle de crédit est au bord du réseau, avant l'activateur : un compte à sec ne démarre pas de pod, et le déclencheur note skipped: no credit. Un cron oublié ne peut pas creuser un solde négatif.
  • Un compte désactivé ou suspendu ne tire pas. Les mêmes règles que pour un appel externe.
  • Le premier appel après une période creuse paie un démarrage à froid. Pour un cron ça n'a aucune importance, personne n'attend la réponse. C'est précisément le profil de charge où le scale-to-zero ne coûte rien en confort.

Et ensuite

Le déclenchement cron n'a pas encore d'écran dans l'interface : il se pilote par l'API, ou par un agent IA branché sur le serveur MCP, qui sait créer la fonction et sa planification dans la même conversation.

Si vous voulez le même exercice avec un état persistant plutôt qu'un webhook, la suite logique est la base de données privée : même modèle de facturation, même mise en veille, et une base que seules vos fonctions peuvent joindre.

Le détail des tarifs est sur la page tarifs, et un compte s'ouvre avec un crédit qui suffit, comme on vient de le calculer, à laisser tourner ce cron bien plus longtemps que vous ne vous souviendrez de son existence.

Essayez sur votre propre compte

L'inscription vient avec du crédit, de quoi déployer, planifier et mesurer tout ce qui précède sans sortir de carte bancaire.

Commencer »