Un cron serverless qui ne coûte rien entre deux exécutions
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.
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.
# 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-varsLe 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.
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 :
# Tous les jours à 7 h UTC.
curl -X POST -H "Authorization: Bearer $JWT" \
-d '{"cron": "0 7 * * *"}' \
https://zerolith.io/api/functions/<id>/schedulesLa 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_secondsde 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 :
mémoire 0,125 Gio × 0,000004 € = 0,0000005 €/s
processeur 0,1 vCPU × 0,0000125 € = 0,00000125 €/s
────────────────
0,00000175 €/sVient 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 :
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 »